@auxiora/runtime 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/dist/agent-router.d.ts +104 -0
- package/dist/agent-router.d.ts.map +1 -0
- package/dist/agent-router.js +128 -0
- package/dist/agent-router.js.map +1 -0
- package/dist/block-stream.d.ts +94 -0
- package/dist/block-stream.d.ts.map +1 -0
- package/dist/block-stream.js +120 -0
- package/dist/block-stream.js.map +1 -0
- package/dist/index.d.ts +149 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2981 -0
- package/dist/index.js.map +1 -0
- package/dist/tool-loop-detection.d.ts +48 -0
- package/dist/tool-loop-detection.d.ts.map +1 -0
- package/dist/tool-loop-detection.js +249 -0
- package/dist/tool-loop-detection.js.map +1 -0
- package/package.json +82 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2981 @@
|
|
|
1
|
+
import { Gateway } from '@auxiora/gateway';
|
|
2
|
+
import { SessionManager, sanitizeTranscript } from '@auxiora/sessions';
|
|
3
|
+
import { MediaProcessor, detectProviders } from '@auxiora/media';
|
|
4
|
+
import { ProviderFactory, readClaudeCliCredentials, isSetupToken, refreshOAuthToken, refreshPKCEOAuthToken, streamWithModelFallback } from '@auxiora/providers';
|
|
5
|
+
import { ModelRouter, TaskClassifier, ModelSelector, CostTracker } from '@auxiora/router';
|
|
6
|
+
import { ChannelManager, DraftStreamLoop } from '@auxiora/channels';
|
|
7
|
+
import { loadConfig, saveConfig as saveFullConfig } from '@auxiora/config';
|
|
8
|
+
import { Vault, vaultExists } from '@auxiora/vault';
|
|
9
|
+
import { audit } from '@auxiora/audit';
|
|
10
|
+
import { getWorkspacePath, getSoulPath, getAgentsPath, getIdentityPath, getUserPath, getBehaviorsPath, getWebhooksPath, getScreenshotsDir, } from '@auxiora/core';
|
|
11
|
+
import { createArchitect, ARCHITECT_BASE_PROMPT, VaultStorageAdapter } from '@auxiora/personality/architect';
|
|
12
|
+
import { toolRegistry, toolExecutor, initializeToolExecutor, ToolPermission, setBrowserManager, setWebhookManager, setBehaviorManager, setProviderFactory, setOrchestrationEngine, setResearchEngine, setClipboardMonitor, setAppController, setSystemStateMonitor, setEmailIntelligence, setCalendarIntelligence, setContactGraph, setContextRecall, setComposeEngine, setGrammarChecker, setLanguageDetector, } from '@auxiora/tools';
|
|
13
|
+
import { ResearchEngine } from '@auxiora/research';
|
|
14
|
+
import { OrchestrationEngine } from '@auxiora/orchestrator';
|
|
15
|
+
import * as crypto from 'node:crypto';
|
|
16
|
+
import * as fs from 'node:fs/promises';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { BehaviorManager } from '@auxiora/behaviors';
|
|
19
|
+
import { BrowserManager } from '@auxiora/browser';
|
|
20
|
+
import { ClipboardMonitor, AppController, SystemStateMonitor } from '@auxiora/os-bridge';
|
|
21
|
+
import { VoiceManager } from '@auxiora/voice';
|
|
22
|
+
import { WhisperSTT } from '@auxiora/stt';
|
|
23
|
+
import { OpenAITTS } from '@auxiora/tts';
|
|
24
|
+
import { WebhookManager } from '@auxiora/webhooks';
|
|
25
|
+
import { createDashboardRouter } from '@auxiora/dashboard';
|
|
26
|
+
import { PluginLoader } from '@auxiora/plugins';
|
|
27
|
+
import { MemoryStore, MemoryRetriever, MemoryExtractor, PatternDetector, PersonalityAdapter, } from '@auxiora/memory';
|
|
28
|
+
import { TrustEngine, ActionAuditTrail, RollbackManager, TrustGate } from '@auxiora/autonomy';
|
|
29
|
+
import { IntentParser, ActionPlanner } from '@auxiora/intent';
|
|
30
|
+
import { createLoopDetectionState, recordToolCall, recordToolOutcome, detectLoop } from './tool-loop-detection.js';
|
|
31
|
+
import { UserManager } from '@auxiora/social';
|
|
32
|
+
import { WorkflowEngine, ApprovalManager, AutonomousExecutor } from '@auxiora/workflows';
|
|
33
|
+
import { AgentProtocol, MessageSigner, AgentDirectory } from '@auxiora/agent-protocol';
|
|
34
|
+
import { AmbientPatternEngine, QuietNotificationManager, BriefingGenerator, AnticipationEngine, AmbientScheduler, DEFAULT_AMBIENT_SCHEDULER_CONFIG, NotificationOrchestrator } from '@auxiora/ambient';
|
|
35
|
+
import { NotificationHub, DoNotDisturbManager } from '@auxiora/notification-hub';
|
|
36
|
+
import { ConnectorRegistry, AuthManager as ConnectorAuthManager, TriggerManager } from '@auxiora/connectors';
|
|
37
|
+
import { googleWorkspaceConnector } from '@auxiora/connector-google-workspace';
|
|
38
|
+
import { microsoftConnector } from '@auxiora/connector-microsoft';
|
|
39
|
+
import { githubConnector } from '@auxiora/connector-github';
|
|
40
|
+
import { linearConnector } from '@auxiora/connector-linear';
|
|
41
|
+
import { notionConnector } from '@auxiora/connector-notion';
|
|
42
|
+
import { homeAssistantConnector } from '@auxiora/connector-homeassistant';
|
|
43
|
+
import { twitterConnector, linkedinConnector, redditConnector, instagramConnector } from '@auxiora/connector-social';
|
|
44
|
+
import { ConversationEngine } from '@auxiora/conversation';
|
|
45
|
+
import { EmailTriageEngine, ThreadSummarizer } from '@auxiora/email-intelligence';
|
|
46
|
+
import { ScheduleAnalyzer, ScheduleOptimizer, MeetingPrepGenerator } from '@auxiora/calendar-intelligence';
|
|
47
|
+
import { ContactGraph, ContextRecall } from '@auxiora/contacts';
|
|
48
|
+
import { ComposeEngine, GrammarChecker, LanguageDetector } from '@auxiora/compose';
|
|
49
|
+
import { ScreenCapturer } from '@auxiora/screen';
|
|
50
|
+
import { CapabilityCatalogImpl, HealthMonitorImpl, createIntrospectTool, generatePromptFragment } from '@auxiora/introspection';
|
|
51
|
+
import { setMemoryStore } from '@auxiora/tools';
|
|
52
|
+
import { getAuditLogger } from '@auxiora/audit';
|
|
53
|
+
import { Router } from 'express';
|
|
54
|
+
import express from 'express';
|
|
55
|
+
import { PersonalityManager, ModeLoader, ModeDetector, PromptAssembler, MODE_IDS, SecurityFloor, ArchitectBridge, ArchitectAwarenessCollector, parseSoulBiases, } from '@auxiora/personality';
|
|
56
|
+
import { getModesDir } from '@auxiora/core';
|
|
57
|
+
import { fileURLToPath } from 'node:url';
|
|
58
|
+
import { getLogger, generateRequestId, runWithRequestId } from '@auxiora/logger';
|
|
59
|
+
import { SelfAwarenessAssembler, InMemoryAwarenessStorage, ConversationReflector, CapacityMonitor, KnowledgeBoundary, RelationshipModel, TemporalTracker, EnvironmentSensor, MetaCognitor, } from '@auxiora/self-awareness';
|
|
60
|
+
/**
|
|
61
|
+
* Map Claude Code emulation tool calls to our actual tool names + input format.
|
|
62
|
+
* The model may call CC tools (WebSearch, Bash, etc.) since they're in the request for OAuth compat.
|
|
63
|
+
*/
|
|
64
|
+
function mapCCToolCall(name, input) {
|
|
65
|
+
switch (name) {
|
|
66
|
+
case 'WebSearch':
|
|
67
|
+
return { name: 'web_browser', input: { url: `https://www.google.com/search?q=${encodeURIComponent(input.query || '')}` } };
|
|
68
|
+
case 'WebFetch':
|
|
69
|
+
return { name: 'web_browser', input: { url: input.url } };
|
|
70
|
+
case 'Bash':
|
|
71
|
+
return { name: 'bash', input: { command: input.command, timeout: input.timeout } };
|
|
72
|
+
case 'Read':
|
|
73
|
+
return { name: 'file_read', input: { path: input.file_path } };
|
|
74
|
+
case 'Write':
|
|
75
|
+
return { name: 'file_write', input: { path: input.file_path, content: input.content } };
|
|
76
|
+
default:
|
|
77
|
+
return { name, input };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const ACTIVITY_EVENT_PREFIXES = [
|
|
81
|
+
'behavior.', 'message.', 'channel.', 'webhook.',
|
|
82
|
+
'system.', 'auth.login', 'auth.logout',
|
|
83
|
+
];
|
|
84
|
+
export class Auxiora {
|
|
85
|
+
logger = getLogger('runtime');
|
|
86
|
+
config;
|
|
87
|
+
gateway;
|
|
88
|
+
sessions;
|
|
89
|
+
providers;
|
|
90
|
+
channels;
|
|
91
|
+
vault;
|
|
92
|
+
systemPrompt = '';
|
|
93
|
+
standardPrompt = '';
|
|
94
|
+
architectPrompt = '';
|
|
95
|
+
running = false;
|
|
96
|
+
behaviors;
|
|
97
|
+
browserManager;
|
|
98
|
+
voiceManager;
|
|
99
|
+
webhookManager;
|
|
100
|
+
pluginLoader;
|
|
101
|
+
modelRouter;
|
|
102
|
+
memoryStore;
|
|
103
|
+
memoryRetriever;
|
|
104
|
+
memoryExtractor;
|
|
105
|
+
patternDetector;
|
|
106
|
+
personalityAdapter;
|
|
107
|
+
mediaProcessor;
|
|
108
|
+
memoryCleanupInterval;
|
|
109
|
+
trustEngine;
|
|
110
|
+
trustAuditTrail;
|
|
111
|
+
rollbackManager;
|
|
112
|
+
trustGate;
|
|
113
|
+
intentParser;
|
|
114
|
+
actionPlanner;
|
|
115
|
+
orchestrationEngine;
|
|
116
|
+
// [P14] Team / Social
|
|
117
|
+
userManager;
|
|
118
|
+
workflowEngine;
|
|
119
|
+
approvalManager;
|
|
120
|
+
autonomousExecutor;
|
|
121
|
+
agentProtocol;
|
|
122
|
+
agentDirectory;
|
|
123
|
+
// [P15] Senses
|
|
124
|
+
ambientEngine;
|
|
125
|
+
ambientNotifications;
|
|
126
|
+
briefingGenerator;
|
|
127
|
+
anticipationEngine;
|
|
128
|
+
conversationEngine;
|
|
129
|
+
screenCapturer;
|
|
130
|
+
screenAnalyzer;
|
|
131
|
+
// Modes system
|
|
132
|
+
modeLoader;
|
|
133
|
+
modeDetector;
|
|
134
|
+
promptAssembler;
|
|
135
|
+
sessionModes = new Map();
|
|
136
|
+
userPreferences;
|
|
137
|
+
// Connector system
|
|
138
|
+
connectorRegistry;
|
|
139
|
+
connectorAuthManager;
|
|
140
|
+
triggerManager;
|
|
141
|
+
ambientScheduler;
|
|
142
|
+
notificationHub;
|
|
143
|
+
dndManager;
|
|
144
|
+
notificationOrchestrator;
|
|
145
|
+
capabilityCatalog;
|
|
146
|
+
healthMonitor;
|
|
147
|
+
capabilityPromptFragment = '';
|
|
148
|
+
selfAwarenessAssembler;
|
|
149
|
+
architect;
|
|
150
|
+
architectBridge = null;
|
|
151
|
+
architectAwarenessCollector = null;
|
|
152
|
+
// Security floor
|
|
153
|
+
securityFloor;
|
|
154
|
+
sessionEscalation = new Map();
|
|
155
|
+
/** Tracks the most recent channel ID for each connected channel type (e.g. discord → snowflake).
|
|
156
|
+
* Used for proactive delivery (behaviors, ambient briefings). Persisted to disk. */
|
|
157
|
+
lastActiveChannels = new Map();
|
|
158
|
+
activeAgents = new Map();
|
|
159
|
+
channelTargetsPath = path.join(path.dirname(getBehaviorsPath()), 'channel-targets.json');
|
|
160
|
+
orchestrationHistory = [];
|
|
161
|
+
async initialize(options = {}) {
|
|
162
|
+
// Load config
|
|
163
|
+
this.config = options.config || (await loadConfig());
|
|
164
|
+
// Initialize vault (optional)
|
|
165
|
+
this.vault = new Vault();
|
|
166
|
+
if (options.vaultPassword) {
|
|
167
|
+
try {
|
|
168
|
+
await this.vault.unlock(options.vaultPassword);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
this.logger.warn('Failed to unlock vault', { error: error instanceof Error ? error : new Error(String(error)) });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Initialize tool system with approval callback
|
|
175
|
+
initializeToolExecutor(async (toolName, params, context) => {
|
|
176
|
+
// For now, auto-approve all tools in non-interactive mode
|
|
177
|
+
// In future, could send approval request to client via WebSocket
|
|
178
|
+
this.logger.debug('Auto-approving tool', { toolName, params });
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
// Initialize sessions
|
|
182
|
+
this.sessions = new SessionManager({
|
|
183
|
+
maxContextTokens: this.config.session.maxContextTokens,
|
|
184
|
+
ttlMinutes: this.config.session.ttlMinutes,
|
|
185
|
+
autoSave: this.config.session.autoSave,
|
|
186
|
+
compactionEnabled: this.config.session.compactionEnabled,
|
|
187
|
+
});
|
|
188
|
+
await this.sessions.initialize();
|
|
189
|
+
// Initialize providers (if vault is unlocked and has keys)
|
|
190
|
+
await this.initializeProviders();
|
|
191
|
+
// Initialize model router (if providers are set up)
|
|
192
|
+
if (this.providers) {
|
|
193
|
+
this.initializeRouter();
|
|
194
|
+
setProviderFactory(this.providers);
|
|
195
|
+
// Inject summarizer for auto-compaction
|
|
196
|
+
this.sessions.setSummarizer(async (prompt) => {
|
|
197
|
+
const provider = this.providers.getPrimaryProvider();
|
|
198
|
+
const result = await provider.complete([{ role: 'user', content: prompt }], { maxTokens: 1024 });
|
|
199
|
+
return result.content;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Initialize orchestration engine (if enabled and providers available)
|
|
203
|
+
if (this.providers && this.config.orchestration?.enabled !== false) {
|
|
204
|
+
this.orchestrationEngine = new OrchestrationEngine(this.providers, this.config.orchestration ?? { enabled: true, maxConcurrentAgents: 5, defaultTimeout: 60000, totalTimeout: 300000, allowedPatterns: ['parallel', 'sequential', 'debate', 'map-reduce', 'supervisor'], costMultiplierWarning: 3 });
|
|
205
|
+
setOrchestrationEngine(this.orchestrationEngine);
|
|
206
|
+
this.logger.info('Orchestration engine initialized');
|
|
207
|
+
}
|
|
208
|
+
// Initialize research engine (if Brave API key available)
|
|
209
|
+
if (this.config.research?.enabled !== false) {
|
|
210
|
+
let vaultKey;
|
|
211
|
+
try {
|
|
212
|
+
vaultKey = this.vault.get('BRAVE_API_KEY');
|
|
213
|
+
}
|
|
214
|
+
catch { /* vault locked */ }
|
|
215
|
+
const braveApiKey = this.config.research?.braveApiKey
|
|
216
|
+
?? vaultKey
|
|
217
|
+
?? process.env.AUXIORA_RESEARCH_BRAVE_API_KEY;
|
|
218
|
+
if (braveApiKey) {
|
|
219
|
+
try {
|
|
220
|
+
const provider = this.providers?.getPrimaryProvider();
|
|
221
|
+
const researchEngine = new ResearchEngine({
|
|
222
|
+
braveApiKey,
|
|
223
|
+
provider: provider ?? undefined,
|
|
224
|
+
defaultDepth: this.config.research?.defaultDepth ?? 'standard',
|
|
225
|
+
maxConcurrentSources: this.config.research?.maxConcurrentSources ?? 5,
|
|
226
|
+
searchTimeout: this.config.research?.searchTimeout ?? 10_000,
|
|
227
|
+
fetchTimeout: this.config.research?.fetchTimeout ?? 15_000,
|
|
228
|
+
});
|
|
229
|
+
setResearchEngine(researchEngine);
|
|
230
|
+
this.logger.info(`Research engine initialized (Brave Search configured${provider ? ', AI extraction enabled' : ''})`);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
this.logger.warn('Failed to initialize research engine', { error: err instanceof Error ? err : new Error(String(err)) });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Initialize channels (if configured and vault is unlocked)
|
|
238
|
+
await this.initializeChannels();
|
|
239
|
+
// Load personality files
|
|
240
|
+
await this.loadPersonality();
|
|
241
|
+
// Initialize modes system
|
|
242
|
+
await this.initializeModes();
|
|
243
|
+
// Initialize gateway
|
|
244
|
+
this.gateway = new Gateway({
|
|
245
|
+
config: this.config,
|
|
246
|
+
needsSetup: async () => {
|
|
247
|
+
if (!(await vaultExists()))
|
|
248
|
+
return true;
|
|
249
|
+
const name = this.config.agent?.name ?? 'Auxiora';
|
|
250
|
+
let hasSoul = false;
|
|
251
|
+
try {
|
|
252
|
+
await fs.access(getSoulPath());
|
|
253
|
+
hasSoul = true;
|
|
254
|
+
}
|
|
255
|
+
catch { }
|
|
256
|
+
return name === 'Auxiora' && !hasSoul;
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
this.gateway.onMessage(this.handleMessage.bind(this));
|
|
260
|
+
// Stream curated audit events to dashboard via WebSocket
|
|
261
|
+
const auditLogger = getAuditLogger();
|
|
262
|
+
auditLogger.onEntry = (entry) => {
|
|
263
|
+
const isActivityEvent = ACTIVITY_EVENT_PREFIXES.some((prefix) => entry.event.startsWith(prefix));
|
|
264
|
+
if (isActivityEvent) {
|
|
265
|
+
this.gateway.broadcast({ type: 'activity', payload: entry }, (client) => client.authenticated);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
// Initialize behavior system
|
|
269
|
+
if (this.providers) {
|
|
270
|
+
this.behaviors = new BehaviorManager({
|
|
271
|
+
storePath: getBehaviorsPath(),
|
|
272
|
+
executorDeps: {
|
|
273
|
+
getProvider: () => this.providers.getPrimaryProvider(),
|
|
274
|
+
sendToChannel: async (channelType, channelId, message) => {
|
|
275
|
+
this.logger.info('sendToChannel called', { channelType, channelId, hasChannels: !!this.channels });
|
|
276
|
+
// Always broadcast to webchat + persist
|
|
277
|
+
this.gateway.broadcast({
|
|
278
|
+
type: 'message',
|
|
279
|
+
payload: { role: 'assistant', content: message.content },
|
|
280
|
+
});
|
|
281
|
+
this.persistToWebchat(message.content);
|
|
282
|
+
// Deliver to all connected external channels
|
|
283
|
+
if (this.channels) {
|
|
284
|
+
const connected = this.channels.getConnectedChannels();
|
|
285
|
+
this.logger.info('Connected channels for delivery', { connected });
|
|
286
|
+
for (const ct of connected) {
|
|
287
|
+
const targetId = this.lastActiveChannels.get(ct)
|
|
288
|
+
?? this.channels.getDefaultChannelId(ct);
|
|
289
|
+
this.logger.info('Channel delivery target', { channel: ct, targetId, fromLastActive: this.lastActiveChannels.get(ct) });
|
|
290
|
+
if (!targetId)
|
|
291
|
+
continue;
|
|
292
|
+
const result = await this.channels.send(ct, targetId, { content: message.content });
|
|
293
|
+
if (!result.success) {
|
|
294
|
+
this.logger.warn('Channel delivery failed', { channel: ct, targetId, error: new Error(result.error ?? 'unknown') });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { success: true };
|
|
299
|
+
},
|
|
300
|
+
getSystemPrompt: () => this.systemPrompt,
|
|
301
|
+
executeWithTools: async (messages, systemPrompt) => {
|
|
302
|
+
const execId = `behavior-exec-${Date.now()}`;
|
|
303
|
+
const actionPreview = messages[0]?.content?.slice(0, 80) ?? 'Behavior';
|
|
304
|
+
this.agentStart(execId, 'behavior', actionPreview);
|
|
305
|
+
// Use unique senderId so getOrCreate never reuses a stale behavior session
|
|
306
|
+
const session = await this.sessions.getOrCreate(execId, {
|
|
307
|
+
channelType: 'behavior',
|
|
308
|
+
senderId: execId,
|
|
309
|
+
clientId: execId,
|
|
310
|
+
});
|
|
311
|
+
for (const msg of messages) {
|
|
312
|
+
await this.sessions.addMessage(session.id, msg.role, msg.content);
|
|
313
|
+
}
|
|
314
|
+
const provider = this.providers.getPrimaryProvider();
|
|
315
|
+
const noopChunk = () => { };
|
|
316
|
+
try {
|
|
317
|
+
const fallbackCandidates = this.providers.resolveFallbackCandidates();
|
|
318
|
+
const result = await this.executeWithTools(session.id, messages, systemPrompt, provider, noopChunk, { fallbackCandidates });
|
|
319
|
+
this.agentEnd(execId, true);
|
|
320
|
+
return { content: result.response, usage: result.usage };
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
this.agentEnd(execId, false);
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
auditFn: (event, details) => {
|
|
329
|
+
audit(event, details);
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
await this.behaviors.start();
|
|
333
|
+
setBehaviorManager(this.behaviors);
|
|
334
|
+
}
|
|
335
|
+
// Initialize browser system
|
|
336
|
+
this.browserManager = new BrowserManager({
|
|
337
|
+
config: {
|
|
338
|
+
headless: true,
|
|
339
|
+
viewport: { width: 1280, height: 720 },
|
|
340
|
+
navigationTimeout: 30_000,
|
|
341
|
+
actionTimeout: 10_000,
|
|
342
|
+
maxConcurrentPages: 10,
|
|
343
|
+
screenshotDir: getScreenshotsDir(),
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
setBrowserManager(this.browserManager);
|
|
347
|
+
// Initialize OS bridge
|
|
348
|
+
const clipboardMonitor = new ClipboardMonitor();
|
|
349
|
+
const appController = new AppController(process.platform);
|
|
350
|
+
const systemStateMonitor = new SystemStateMonitor();
|
|
351
|
+
setClipboardMonitor(clipboardMonitor);
|
|
352
|
+
setAppController(appController);
|
|
353
|
+
setSystemStateMonitor(systemStateMonitor);
|
|
354
|
+
this.logger.info('OS bridge initialized');
|
|
355
|
+
// Initialize email intelligence (engines ready; connectors needed for full functionality)
|
|
356
|
+
const emailTriageEngine = new EmailTriageEngine();
|
|
357
|
+
const threadSummarizer = new ThreadSummarizer();
|
|
358
|
+
setEmailIntelligence({
|
|
359
|
+
triage: {
|
|
360
|
+
getTriageSummary: async () => ({
|
|
361
|
+
categories: { urgent: [], action: [], fyi: [], newsletter: [], spam: [] },
|
|
362
|
+
total: 0,
|
|
363
|
+
message: 'Connect an email account to enable triage. Use /connect google-workspace',
|
|
364
|
+
}),
|
|
365
|
+
engine: emailTriageEngine,
|
|
366
|
+
},
|
|
367
|
+
summarizer: {
|
|
368
|
+
summarizeThread: async () => ({
|
|
369
|
+
summary: 'Connect an email account to enable thread summarization.',
|
|
370
|
+
}),
|
|
371
|
+
engine: threadSummarizer,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
this.logger.info('Email intelligence initialized');
|
|
375
|
+
// Initialize calendar intelligence (engines ready; connectors needed for full functionality)
|
|
376
|
+
const scheduleAnalyzer = new ScheduleAnalyzer();
|
|
377
|
+
const scheduleOptimizer = new ScheduleOptimizer();
|
|
378
|
+
const meetingPrepGenerator = new MeetingPrepGenerator();
|
|
379
|
+
setCalendarIntelligence({
|
|
380
|
+
analyzeDay: async (date) => scheduleAnalyzer.analyzeDay([], date),
|
|
381
|
+
suggest: (analysis) => scheduleOptimizer.suggest(analysis),
|
|
382
|
+
getMeetingBrief: async () => ({
|
|
383
|
+
message: 'Connect a calendar account to enable meeting preparation. Use /connect google-workspace',
|
|
384
|
+
}),
|
|
385
|
+
analyzer: scheduleAnalyzer,
|
|
386
|
+
optimizer: scheduleOptimizer,
|
|
387
|
+
meetingPrep: meetingPrepGenerator,
|
|
388
|
+
});
|
|
389
|
+
this.logger.info('Calendar intelligence initialized');
|
|
390
|
+
// Initialize contacts system
|
|
391
|
+
const contactGraph = new ContactGraph();
|
|
392
|
+
const contextRecall = new ContextRecall(contactGraph);
|
|
393
|
+
setContactGraph(contactGraph);
|
|
394
|
+
setContextRecall(contextRecall);
|
|
395
|
+
this.logger.info('Contacts system initialized');
|
|
396
|
+
// Initialize compose system
|
|
397
|
+
const composeEngine = new ComposeEngine();
|
|
398
|
+
const grammarChecker = new GrammarChecker();
|
|
399
|
+
const languageDetector = new LanguageDetector();
|
|
400
|
+
setComposeEngine(composeEngine);
|
|
401
|
+
setGrammarChecker(grammarChecker);
|
|
402
|
+
setLanguageDetector(languageDetector);
|
|
403
|
+
this.logger.info('Compose system initialized');
|
|
404
|
+
// Initialize voice system (if enabled and OpenAI key available)
|
|
405
|
+
if (this.config.voice?.enabled) {
|
|
406
|
+
let openaiKeyForVoice;
|
|
407
|
+
try {
|
|
408
|
+
openaiKeyForVoice = this.vault.get('OPENAI_API_KEY');
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Vault locked
|
|
412
|
+
}
|
|
413
|
+
if (openaiKeyForVoice) {
|
|
414
|
+
this.voiceManager = new VoiceManager({
|
|
415
|
+
sttProvider: new WhisperSTT({ apiKey: openaiKeyForVoice }),
|
|
416
|
+
ttsProvider: new OpenAITTS({
|
|
417
|
+
apiKey: openaiKeyForVoice,
|
|
418
|
+
defaultVoice: this.config.voice.defaultVoice,
|
|
419
|
+
}),
|
|
420
|
+
config: {
|
|
421
|
+
enabled: true,
|
|
422
|
+
defaultVoice: this.config.voice.defaultVoice,
|
|
423
|
+
language: this.config.voice.language,
|
|
424
|
+
maxAudioDuration: this.config.voice.maxAudioDuration,
|
|
425
|
+
sampleRate: this.config.voice.sampleRate,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
this.gateway.onVoiceMessage(this.handleVoiceMessage.bind(this));
|
|
429
|
+
this.logger.info('Voice mode enabled');
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
this.logger.warn('Voice mode enabled in config but no OPENAI_API_KEY found in vault');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Initialize webhook system (if enabled)
|
|
436
|
+
if (this.config.webhooks?.enabled) {
|
|
437
|
+
this.webhookManager = new WebhookManager({
|
|
438
|
+
storePath: getWebhooksPath(),
|
|
439
|
+
config: {
|
|
440
|
+
enabled: true,
|
|
441
|
+
basePath: this.config.webhooks.basePath,
|
|
442
|
+
signatureHeader: this.config.webhooks.signatureHeader,
|
|
443
|
+
maxPayloadSize: this.config.webhooks.maxPayloadSize,
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
setWebhookManager(this.webhookManager);
|
|
447
|
+
// Wire behavior trigger
|
|
448
|
+
if (this.behaviors) {
|
|
449
|
+
this.webhookManager.setBehaviorTrigger(async (behaviorId, payload) => {
|
|
450
|
+
return this.behaviors.executeNow(behaviorId);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
// Mount webhook routes
|
|
454
|
+
const webhookRouter = this.createWebhookRouter();
|
|
455
|
+
this.gateway.mountRouter(this.config.webhooks.basePath, webhookRouter);
|
|
456
|
+
this.logger.info('Webhook listeners enabled');
|
|
457
|
+
}
|
|
458
|
+
// Initialize dashboard (if enabled)
|
|
459
|
+
if (this.config.dashboard?.enabled) {
|
|
460
|
+
// Capture class reference for lazy getters (getters rebind `this` to the deps object)
|
|
461
|
+
const self = this;
|
|
462
|
+
const { router } = createDashboardRouter({
|
|
463
|
+
deps: {
|
|
464
|
+
vault: this.vault,
|
|
465
|
+
getActiveModel: () => {
|
|
466
|
+
const primary = this.config.provider.primary;
|
|
467
|
+
const fallback = this.config.provider.fallback;
|
|
468
|
+
const providerConfig = this.config.provider[primary];
|
|
469
|
+
return { provider: primary, fallback, model: providerConfig?.model ?? 'default' };
|
|
470
|
+
},
|
|
471
|
+
onVaultUnlocked: async () => {
|
|
472
|
+
// Re-initialize providers and channels after vault unlock on restart
|
|
473
|
+
await this.initializeProviders();
|
|
474
|
+
if (this.providers) {
|
|
475
|
+
this.initializeRouter();
|
|
476
|
+
setProviderFactory(this.providers);
|
|
477
|
+
}
|
|
478
|
+
const channels = this.channels;
|
|
479
|
+
if (!channels) {
|
|
480
|
+
await this.initializeChannels();
|
|
481
|
+
const ch = this.channels;
|
|
482
|
+
if (ch) {
|
|
483
|
+
await ch.connectAll();
|
|
484
|
+
this.logger.info('Channels connected after vault unlock');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Restore connector tokens now that vault is accessible
|
|
488
|
+
if (this.connectorRegistry && this.connectorAuthManager) {
|
|
489
|
+
for (const connector of this.connectorRegistry.list()) {
|
|
490
|
+
try {
|
|
491
|
+
const tokenData = this.vault.get(`connectors.${connector.id}.tokens`);
|
|
492
|
+
if (tokenData) {
|
|
493
|
+
const tokens = typeof tokenData === 'string' ? JSON.parse(tokenData) : tokenData;
|
|
494
|
+
await this.connectorAuthManager.authenticate(connector.id, connector.auth, tokens);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
this.logger.warn(`Failed to restore tokens for connector ${connector.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
this.registerConnectorTools();
|
|
502
|
+
this.logger.info('Connector tools registered after vault unlock');
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
behaviors: this.behaviors ? {
|
|
506
|
+
list: (filter) => this.behaviors.list(filter),
|
|
507
|
+
get: (id) => this.behaviors.get(id),
|
|
508
|
+
create: (input) => this.behaviors.create(input),
|
|
509
|
+
update: (id, updates) => this.behaviors.update(id, updates),
|
|
510
|
+
remove: (id) => this.behaviors.remove(id),
|
|
511
|
+
} : undefined,
|
|
512
|
+
webhooks: this.webhookManager ? {
|
|
513
|
+
list: () => this.webhookManager.list(),
|
|
514
|
+
create: (options) => this.webhookManager.create(options),
|
|
515
|
+
update: (id, updates) => this.webhookManager.update(id, updates),
|
|
516
|
+
delete: (id) => this.webhookManager.delete(id),
|
|
517
|
+
} : undefined,
|
|
518
|
+
getConfiguredChannels: () => {
|
|
519
|
+
const ch = this.config.channels;
|
|
520
|
+
return Object.entries(ch)
|
|
521
|
+
.filter(([, v]) => typeof v === 'object' && v !== null)
|
|
522
|
+
.map(([type, v]) => ({ type, enabled: !!v.enabled }));
|
|
523
|
+
},
|
|
524
|
+
getActiveAgents: () => this.getActiveAgents(),
|
|
525
|
+
getHealthState: () => this.healthMonitor?.getHealthState() ?? { overall: 'unknown', subsystems: [], issues: [], lastCheck: '' },
|
|
526
|
+
getCapabilities: () => this.capabilityCatalog?.getCatalog() ?? null,
|
|
527
|
+
getConnections: () => this.gateway.getConnections(),
|
|
528
|
+
getAuditEntries: async (limit) => {
|
|
529
|
+
const auditLogger = getAuditLogger();
|
|
530
|
+
return auditLogger.getEntries(limit);
|
|
531
|
+
},
|
|
532
|
+
getPlugins: () => this.pluginLoader?.listPlugins() ?? [],
|
|
533
|
+
getMemories: async () => this.memoryStore?.getAll() ?? [],
|
|
534
|
+
get connectors() {
|
|
535
|
+
const reg = self.connectorRegistry;
|
|
536
|
+
const auth = self.connectorAuthManager;
|
|
537
|
+
if (!reg || !auth)
|
|
538
|
+
return undefined;
|
|
539
|
+
return {
|
|
540
|
+
list: () => reg.list().map(c => ({
|
|
541
|
+
id: c.id, name: c.name, category: c.category,
|
|
542
|
+
auth: { type: c.auth.type },
|
|
543
|
+
})),
|
|
544
|
+
get: (id) => reg.get(id),
|
|
545
|
+
connect: async (connectorId, credentials) => {
|
|
546
|
+
const connector = reg.get(connectorId);
|
|
547
|
+
if (!connector)
|
|
548
|
+
return null;
|
|
549
|
+
return auth.authenticate(connectorId, connector.auth, credentials);
|
|
550
|
+
},
|
|
551
|
+
disconnect: async (connectorId) => {
|
|
552
|
+
return auth.revokeToken(connectorId);
|
|
553
|
+
},
|
|
554
|
+
getActions: (connectorId) => reg.getActions(connectorId),
|
|
555
|
+
executeAction: async (connectorId, actionId, params) => {
|
|
556
|
+
const connector = reg.get(connectorId);
|
|
557
|
+
if (!connector)
|
|
558
|
+
return { success: false, error: 'Connector not found' };
|
|
559
|
+
const token = auth.getToken(connectorId);
|
|
560
|
+
if (!token)
|
|
561
|
+
return { success: false, error: 'Not authenticated' };
|
|
562
|
+
try {
|
|
563
|
+
const data = await connector.executeAction(actionId, params, token.accessToken);
|
|
564
|
+
return { success: true, data };
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
},
|
|
572
|
+
models: this.providers ? {
|
|
573
|
+
listProviders: () => {
|
|
574
|
+
const result = [];
|
|
575
|
+
for (const name of this.providers.listAvailable()) {
|
|
576
|
+
const p = this.providers.getProvider(name);
|
|
577
|
+
// Detect credential source for Anthropic
|
|
578
|
+
let credentialSource;
|
|
579
|
+
if (name === 'anthropic') {
|
|
580
|
+
try {
|
|
581
|
+
if (this.vault.get('ANTHROPIC_OAUTH_TOKEN') || (this.vault.get('ANTHROPIC_API_KEY') && isSetupToken(this.vault.get('ANTHROPIC_API_KEY')))) {
|
|
582
|
+
credentialSource = 'oauth';
|
|
583
|
+
}
|
|
584
|
+
else if (this.vault.get('ANTHROPIC_API_KEY')) {
|
|
585
|
+
credentialSource = 'api-key';
|
|
586
|
+
}
|
|
587
|
+
else if (readClaudeCliCredentials() !== null) {
|
|
588
|
+
credentialSource = 'claude-cli';
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
// vault locked — check CLI as last resort
|
|
593
|
+
if (readClaudeCliCredentials() !== null) {
|
|
594
|
+
credentialSource = 'claude-cli';
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
result.push({
|
|
599
|
+
name,
|
|
600
|
+
displayName: credentialSource === 'claude-cli' ? 'Anthropic Claude (via Claude Code)' : p.metadata.displayName,
|
|
601
|
+
available: true,
|
|
602
|
+
models: p.metadata.models,
|
|
603
|
+
credentialSource,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
607
|
+
},
|
|
608
|
+
getRoutingConfig: () => ({
|
|
609
|
+
enabled: this.config.routing?.enabled !== false,
|
|
610
|
+
primary: this.config.provider.primary,
|
|
611
|
+
fallback: this.config.provider.fallback,
|
|
612
|
+
defaultModel: this.config.routing?.defaultModel,
|
|
613
|
+
rules: this.config.routing?.rules ?? [],
|
|
614
|
+
preferences: this.config.routing?.preferences ?? {},
|
|
615
|
+
costLimits: this.config.routing?.costLimits ?? {},
|
|
616
|
+
}),
|
|
617
|
+
getCostSummary: () => this.modelRouter?.getCostSummary() ?? {
|
|
618
|
+
today: 0, thisMonth: 0, isOverBudget: false, warningThresholdReached: false,
|
|
619
|
+
},
|
|
620
|
+
} : undefined,
|
|
621
|
+
orchestration: this.orchestrationEngine ? {
|
|
622
|
+
getConfig: () => ({
|
|
623
|
+
enabled: this.config.orchestration?.enabled !== false,
|
|
624
|
+
maxConcurrentAgents: this.config.orchestration?.maxConcurrentAgents ?? 5,
|
|
625
|
+
allowedPatterns: this.config.orchestration?.allowedPatterns ?? [],
|
|
626
|
+
}),
|
|
627
|
+
getHistory: (limit) => {
|
|
628
|
+
const max = limit ?? 20;
|
|
629
|
+
return this.orchestrationHistory.slice(-max);
|
|
630
|
+
},
|
|
631
|
+
} : undefined,
|
|
632
|
+
memory: {
|
|
633
|
+
getLivingState: async () => {
|
|
634
|
+
const state = await this.getLivingMemoryState();
|
|
635
|
+
return state ?? { facts: [], relationships: [], patterns: [], adaptations: [], stats: { totalMemories: 0, oldestMemory: 0, newestMemory: 0, averageImportance: 0, topTags: [] } };
|
|
636
|
+
},
|
|
637
|
+
getStats: async () => this.memoryStore ? this.memoryStore.getStats() : {},
|
|
638
|
+
getAdaptations: async () => this.personalityAdapter ? this.personalityAdapter.getAdjustments() : [],
|
|
639
|
+
deleteMemory: async (id) => this.memoryStore ? this.memoryStore.remove(id) : false,
|
|
640
|
+
exportAll: async () => this.memoryStore ? this.memoryStore.exportAll() : { version: '1.0', memories: [], exportedAt: Date.now() },
|
|
641
|
+
importAll: async (data) => this.memoryStore ? this.memoryStore.importAll(data) : { imported: 0, skipped: 0 },
|
|
642
|
+
},
|
|
643
|
+
sessions: {
|
|
644
|
+
getWebchatMessages: async () => {
|
|
645
|
+
// Use getOrCreate so the session is loaded from disk if not yet in memory
|
|
646
|
+
const webchat = await this.sessions.getOrCreate('webchat', {
|
|
647
|
+
channelType: 'webchat',
|
|
648
|
+
});
|
|
649
|
+
return webchat.messages
|
|
650
|
+
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
651
|
+
.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp }));
|
|
652
|
+
},
|
|
653
|
+
listChats: (options) => this.sessions.listChats(options),
|
|
654
|
+
createChat: (title) => this.sessions.createChat(title),
|
|
655
|
+
renameChat: (chatId, title) => this.sessions.renameChat(chatId, title),
|
|
656
|
+
archiveChat: (chatId) => this.sessions.archiveChat(chatId),
|
|
657
|
+
deleteChat: (chatId) => this.sessions.deleteChat(chatId),
|
|
658
|
+
getChatMessages: (chatId) => this.sessions.getChatMessages(chatId),
|
|
659
|
+
updateChatMetadata: (chatId, metadata) => this.sessions.updateChatMetadata(chatId, metadata),
|
|
660
|
+
},
|
|
661
|
+
getPersonalityEngine: () => this.config.agent.personality ?? 'standard',
|
|
662
|
+
setPersonalityEngine: (engine) => this.setPersonalityEngine(engine),
|
|
663
|
+
get trust() {
|
|
664
|
+
return self.trustEngine && self.trustAuditTrail && self.rollbackManager ? {
|
|
665
|
+
getLevels: () => self.trustEngine.getAllLevels(),
|
|
666
|
+
getLevel: (domain) => self.trustEngine.getTrustLevel(domain),
|
|
667
|
+
setLevel: async (domain, level, reason) => {
|
|
668
|
+
await self.trustEngine.setTrustLevel(domain, level, reason);
|
|
669
|
+
},
|
|
670
|
+
getAuditEntries: (limit) => self.trustAuditTrail.query({ limit }),
|
|
671
|
+
getAuditEntry: (id) => self.trustAuditTrail.getById(id),
|
|
672
|
+
rollback: async (id) => self.rollbackManager.rollback(id),
|
|
673
|
+
getPromotions: () => self.trustEngine.getPromotions(),
|
|
674
|
+
} : undefined;
|
|
675
|
+
},
|
|
676
|
+
// [P14] Team / Social
|
|
677
|
+
get team() {
|
|
678
|
+
return self.userManager ? {
|
|
679
|
+
listUsers: () => self.userManager.listUsers(),
|
|
680
|
+
createUser: (name, role, channels) => self.userManager.createUser(name, role, { channels }),
|
|
681
|
+
deleteUser: (id) => self.userManager.deleteUser(id),
|
|
682
|
+
} : undefined;
|
|
683
|
+
},
|
|
684
|
+
// [P14] Workflows
|
|
685
|
+
get workflows() {
|
|
686
|
+
return self.workflowEngine && self.approvalManager ? {
|
|
687
|
+
listActive: () => self.workflowEngine.listActive(),
|
|
688
|
+
listAll: () => self.workflowEngine.listAll(),
|
|
689
|
+
getStatus: (id) => self.workflowEngine.getStatus(id),
|
|
690
|
+
createWorkflow: (options) => self.workflowEngine.createWorkflow(options),
|
|
691
|
+
completeStep: (wfId, stepId, completedBy) => self.workflowEngine.completeStep(wfId, stepId, completedBy),
|
|
692
|
+
cancelWorkflow: (id) => self.workflowEngine.cancelWorkflow(id),
|
|
693
|
+
getPendingApprovals: (userId) => self.approvalManager.getPending(userId),
|
|
694
|
+
approve: (id, userId, reason) => self.approvalManager.approve(id, userId, reason),
|
|
695
|
+
reject: (id, userId, reason) => self.approvalManager.reject(id, userId, reason),
|
|
696
|
+
} : undefined;
|
|
697
|
+
},
|
|
698
|
+
// [P14] Agent Protocol
|
|
699
|
+
get agentProtocol() {
|
|
700
|
+
return self.agentProtocol && self.agentDirectory ? {
|
|
701
|
+
getIdentity: () => self.agentProtocol.getIdentity(),
|
|
702
|
+
getInbox: (limit) => self.agentProtocol.getInbox(limit),
|
|
703
|
+
discover: (query) => self.agentProtocol.discover(query),
|
|
704
|
+
getDirectory: () => self.agentDirectory.listAll(),
|
|
705
|
+
} : undefined;
|
|
706
|
+
},
|
|
707
|
+
// [P15] Screen
|
|
708
|
+
get screen() {
|
|
709
|
+
return self.screenCapturer ? {
|
|
710
|
+
capture: async () => {
|
|
711
|
+
const cap = await self.screenCapturer.captureScreen();
|
|
712
|
+
return { image: cap.image.toString('base64'), dimensions: cap.dimensions };
|
|
713
|
+
},
|
|
714
|
+
analyze: async (question) => {
|
|
715
|
+
if (!self.screenAnalyzer)
|
|
716
|
+
return 'Screen analyzer not available';
|
|
717
|
+
const cap = await self.screenCapturer.captureScreen();
|
|
718
|
+
return self.screenAnalyzer.analyzeScreen(cap.image, question);
|
|
719
|
+
},
|
|
720
|
+
} : undefined;
|
|
721
|
+
},
|
|
722
|
+
// [P15] Ambient
|
|
723
|
+
get ambient() {
|
|
724
|
+
return self.ambientEngine && self.ambientNotifications ? {
|
|
725
|
+
getPatterns: () => self.ambientEngine.getPatterns(),
|
|
726
|
+
getNotifications: () => self.ambientNotifications.getQueue(),
|
|
727
|
+
dismissNotification: (id) => self.ambientNotifications.dismiss(id),
|
|
728
|
+
getBriefing: (time) => {
|
|
729
|
+
return self.briefingGenerator.generateBriefing('dashboard', time === 'evening' ? 'evening' : 'morning', {
|
|
730
|
+
patterns: self.ambientEngine.getPatterns(),
|
|
731
|
+
notifications: self.ambientNotifications.getQueue(),
|
|
732
|
+
anticipations: self.anticipationEngine.getAnticipations(),
|
|
733
|
+
});
|
|
734
|
+
},
|
|
735
|
+
getAnticipations: () => self.anticipationEngine.getAnticipations(),
|
|
736
|
+
} : undefined;
|
|
737
|
+
},
|
|
738
|
+
// [P15] Conversation
|
|
739
|
+
get conversation() {
|
|
740
|
+
return self.conversationEngine ? {
|
|
741
|
+
getState: () => self.conversationEngine.getState(),
|
|
742
|
+
start: () => self.conversationEngine.start(),
|
|
743
|
+
stop: () => self.conversationEngine.stop(),
|
|
744
|
+
getTurnCount: () => self.conversationEngine.getTurnCount(),
|
|
745
|
+
} : undefined;
|
|
746
|
+
},
|
|
747
|
+
setup: {
|
|
748
|
+
personality: (() => {
|
|
749
|
+
const mgr = new PersonalityManager(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../personality/templates'), getWorkspacePath());
|
|
750
|
+
return {
|
|
751
|
+
listTemplates: () => mgr.listTemplates(),
|
|
752
|
+
applyTemplate: (id) => mgr.applyTemplate(id),
|
|
753
|
+
buildCustom: (config) => mgr.buildCustom(config),
|
|
754
|
+
getActiveTemplate: async () => {
|
|
755
|
+
const soulPath = path.join(getWorkspacePath(), 'SOUL.md');
|
|
756
|
+
let content;
|
|
757
|
+
try {
|
|
758
|
+
content = await fs.readFile(soulPath, 'utf-8');
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
// Read template ID from frontmatter
|
|
764
|
+
const match = content.match(/^---\n[\s\S]*?template:\s*(\S+)/);
|
|
765
|
+
if (!match)
|
|
766
|
+
return null;
|
|
767
|
+
const templateId = match[1];
|
|
768
|
+
const template = await mgr.getTemplate(templateId);
|
|
769
|
+
return template ? { id: template.id, name: template.name } : null;
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
})(),
|
|
773
|
+
saveConfig: async (updates) => {
|
|
774
|
+
const current = await loadConfig();
|
|
775
|
+
const merged = deepMerge(current, updates);
|
|
776
|
+
await saveFullConfig(merged);
|
|
777
|
+
this.config = await loadConfig();
|
|
778
|
+
// Rebuild system prompt if agent identity changed
|
|
779
|
+
if (updates.agent) {
|
|
780
|
+
await this.loadPersonality();
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
getAgentName: () => this.config.agent?.name ?? 'Auxiora',
|
|
784
|
+
getAgentPronouns: () => this.config.agent?.pronouns ?? 'they/them',
|
|
785
|
+
getAgentConfig: () => (this.config.agent ?? {}),
|
|
786
|
+
getSoulContent: async () => {
|
|
787
|
+
try {
|
|
788
|
+
return await fs.readFile(getSoulPath(), 'utf-8');
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
saveSoulContent: async (content) => {
|
|
795
|
+
const soulPath = getSoulPath();
|
|
796
|
+
const dir = path.dirname(soulPath);
|
|
797
|
+
await fs.mkdir(dir, { recursive: true });
|
|
798
|
+
await fs.writeFile(soulPath, content, 'utf-8');
|
|
799
|
+
},
|
|
800
|
+
hasSoulFile: async () => {
|
|
801
|
+
try {
|
|
802
|
+
await fs.access(getSoulPath());
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
vaultExists: () => vaultExists(),
|
|
810
|
+
onSetupComplete: async () => {
|
|
811
|
+
await this.initializeProviders();
|
|
812
|
+
if (this.providers) {
|
|
813
|
+
this.initializeRouter();
|
|
814
|
+
setProviderFactory(this.providers);
|
|
815
|
+
this.logger.info('Providers re-initialized after setup');
|
|
816
|
+
}
|
|
817
|
+
// Reload personality so the agent uses the name entered during setup
|
|
818
|
+
await this.loadPersonality();
|
|
819
|
+
this.logger.info('Personality reloaded after setup');
|
|
820
|
+
// Connect channels now that vault is unlocked with credentials
|
|
821
|
+
await this.initializeChannels();
|
|
822
|
+
const channels = this.channels;
|
|
823
|
+
if (channels) {
|
|
824
|
+
await channels.connectAll();
|
|
825
|
+
this.logger.info('Channels connected after setup');
|
|
826
|
+
}
|
|
827
|
+
// Restore connector tokens now that vault is accessible
|
|
828
|
+
if (this.connectorRegistry && this.connectorAuthManager) {
|
|
829
|
+
for (const connector of this.connectorRegistry.list()) {
|
|
830
|
+
try {
|
|
831
|
+
const tokenData = this.vault.get(`connectors.${connector.id}.tokens`);
|
|
832
|
+
if (tokenData) {
|
|
833
|
+
const tokens = typeof tokenData === 'string' ? JSON.parse(tokenData) : tokenData;
|
|
834
|
+
await this.connectorAuthManager.authenticate(connector.id, connector.auth, tokens);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
this.logger.warn(`Failed to restore tokens for connector ${connector.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
this.registerConnectorTools();
|
|
842
|
+
this.logger.info('Connector tools registered after setup');
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
config: {
|
|
848
|
+
enabled: true,
|
|
849
|
+
sessionTtlMs: this.config.dashboard.sessionTtlMs,
|
|
850
|
+
},
|
|
851
|
+
verifyPassword: (input) => {
|
|
852
|
+
try {
|
|
853
|
+
const stored = this.vault.get('DASHBOARD_PASSWORD');
|
|
854
|
+
if (!stored)
|
|
855
|
+
return false;
|
|
856
|
+
const a = Buffer.from(stored, 'utf-8');
|
|
857
|
+
const b = Buffer.from(input, 'utf-8');
|
|
858
|
+
if (a.length !== b.length)
|
|
859
|
+
return false;
|
|
860
|
+
return crypto.timingSafeEqual(a, b);
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
return false; // Vault locked
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
this.gateway.mountRouter('/api/v1/dashboard', router);
|
|
868
|
+
// Serve static SPA files
|
|
869
|
+
const dashboardUiPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../dashboard/dist-ui');
|
|
870
|
+
this.gateway.mountRouter('/dashboard', express.static(dashboardUiPath));
|
|
871
|
+
// SPA catch-all: serve index.html for client-side routing
|
|
872
|
+
const dashboardIndexPath = path.join(dashboardUiPath, 'index.html');
|
|
873
|
+
const spaRouter = Router();
|
|
874
|
+
spaRouter.get('/{*splat}', (_req, res) => {
|
|
875
|
+
res.sendFile(dashboardIndexPath);
|
|
876
|
+
});
|
|
877
|
+
this.gateway.mountRouter('/dashboard', spaRouter);
|
|
878
|
+
this.logger.info('Dashboard enabled at /dashboard');
|
|
879
|
+
}
|
|
880
|
+
// Initialize plugin system (if enabled)
|
|
881
|
+
if (this.config.plugins?.enabled !== false) {
|
|
882
|
+
const pluginsDir = this.config.plugins?.dir || undefined;
|
|
883
|
+
this.pluginLoader = new PluginLoader(pluginsDir);
|
|
884
|
+
const loaded = await this.pluginLoader.loadAll();
|
|
885
|
+
const successful = loaded.filter(p => p.status === 'loaded');
|
|
886
|
+
if (loaded.length > 0) {
|
|
887
|
+
this.logger.info(`Plugins: ${successful.length} loaded, ${loaded.length - successful.length} failed`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// Initialize memory system (if enabled)
|
|
891
|
+
if (this.config.memory?.enabled !== false) {
|
|
892
|
+
this.memoryStore = new MemoryStore({
|
|
893
|
+
maxEntries: this.config.memory?.maxEntries,
|
|
894
|
+
});
|
|
895
|
+
this.memoryRetriever = new MemoryRetriever();
|
|
896
|
+
this.patternDetector = new PatternDetector();
|
|
897
|
+
this.personalityAdapter = new PersonalityAdapter(this.memoryStore);
|
|
898
|
+
setMemoryStore(this.memoryStore);
|
|
899
|
+
// Create extractor with AI provider (needs providers initialized)
|
|
900
|
+
if (this.providers && this.providers.listAvailable().length > 0) {
|
|
901
|
+
try {
|
|
902
|
+
this.memoryExtractor = new MemoryExtractor(this.memoryStore, this.providers.getPrimaryProvider());
|
|
903
|
+
}
|
|
904
|
+
catch {
|
|
905
|
+
this.logger.warn('Memory extractor disabled: no AI provider available');
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// Set up periodic cleanup of expired memories
|
|
909
|
+
const cleanupMinutes = this.config.memory?.cleanupIntervalMinutes;
|
|
910
|
+
if (cleanupMinutes) {
|
|
911
|
+
this.memoryCleanupInterval = setInterval(() => void this.memoryStore?.cleanExpired(), cleanupMinutes * 60 * 1000);
|
|
912
|
+
}
|
|
913
|
+
this.logger.info('Memory system enabled (living memory)');
|
|
914
|
+
if (this.pluginLoader) {
|
|
915
|
+
this.pluginLoader.setMemoryStore(this.memoryStore);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// Initialize trust engine (if enabled)
|
|
919
|
+
if (this.config.trust?.enabled !== false) {
|
|
920
|
+
this.trustEngine = new TrustEngine({
|
|
921
|
+
defaultLevel: (this.config.trust?.defaultLevel ?? 0),
|
|
922
|
+
autoPromote: this.config.trust?.autoPromote ?? true,
|
|
923
|
+
promotionThreshold: this.config.trust?.promotionThreshold ?? 10,
|
|
924
|
+
demotionThreshold: this.config.trust?.demotionThreshold ?? 3,
|
|
925
|
+
autoPromoteCeiling: (this.config.trust?.autoPromoteCeiling ?? 3),
|
|
926
|
+
});
|
|
927
|
+
await this.trustEngine.load();
|
|
928
|
+
this.trustAuditTrail = new ActionAuditTrail();
|
|
929
|
+
await this.trustAuditTrail.load();
|
|
930
|
+
this.rollbackManager = new RollbackManager(this.trustAuditTrail);
|
|
931
|
+
this.trustGate = new TrustGate(this.trustEngine);
|
|
932
|
+
this.logger.info('Trust engine initialized');
|
|
933
|
+
}
|
|
934
|
+
// Initialize intent parser (if enabled)
|
|
935
|
+
if (this.config.intent?.enabled !== false) {
|
|
936
|
+
this.intentParser = new IntentParser({
|
|
937
|
+
confidenceThreshold: this.config.intent?.confidenceThreshold ?? 0.3,
|
|
938
|
+
});
|
|
939
|
+
this.actionPlanner = new ActionPlanner();
|
|
940
|
+
this.logger.info('Intent parser initialized');
|
|
941
|
+
}
|
|
942
|
+
// [P14] Initialize team / social system
|
|
943
|
+
this.userManager = new UserManager();
|
|
944
|
+
this.workflowEngine = new WorkflowEngine();
|
|
945
|
+
this.approvalManager = new ApprovalManager();
|
|
946
|
+
this.logger.info('Team/social system initialized');
|
|
947
|
+
// [P14] Initialize agent protocol
|
|
948
|
+
const agentKeys = MessageSigner.generateKeyPair();
|
|
949
|
+
const agentSigner = new MessageSigner(agentKeys);
|
|
950
|
+
this.agentDirectory = new AgentDirectory();
|
|
951
|
+
const agentName = this.config.agent?.name ?? 'auxiora';
|
|
952
|
+
const agentHost = `${this.config.gateway.host}:${this.config.gateway.port}`;
|
|
953
|
+
const agentId = { user: agentName.toLowerCase(), host: agentHost };
|
|
954
|
+
await this.agentDirectory.register(agentId, agentName, agentKeys.publicKey, `http://${agentHost}/api/v1/agent-protocol`);
|
|
955
|
+
this.agentProtocol = new AgentProtocol(agentId, agentSigner, this.agentDirectory);
|
|
956
|
+
this.logger.info('Agent protocol initialized');
|
|
957
|
+
// [P15] Initialize ambient intelligence
|
|
958
|
+
this.ambientEngine = new AmbientPatternEngine();
|
|
959
|
+
this.ambientNotifications = new QuietNotificationManager();
|
|
960
|
+
this.briefingGenerator = new BriefingGenerator();
|
|
961
|
+
this.anticipationEngine = new AnticipationEngine();
|
|
962
|
+
this.logger.info('Ambient intelligence initialized');
|
|
963
|
+
// Initialize notification orchestrator
|
|
964
|
+
this.notificationHub = new NotificationHub();
|
|
965
|
+
this.dndManager = new DoNotDisturbManager();
|
|
966
|
+
this.notificationOrchestrator = new NotificationOrchestrator(this.notificationHub, this.dndManager, (notification) => {
|
|
967
|
+
this.gateway.broadcast({
|
|
968
|
+
type: 'notification',
|
|
969
|
+
payload: { content: notification.message, system: true },
|
|
970
|
+
});
|
|
971
|
+
this.deliverToAllChannels(notification.message);
|
|
972
|
+
});
|
|
973
|
+
this.logger.info('Notification orchestrator initialized');
|
|
974
|
+
// Initialize autonomous workflow executor
|
|
975
|
+
if (this.workflowEngine && this.trustGate && this.trustEngine && this.trustAuditTrail) {
|
|
976
|
+
this.autonomousExecutor = new AutonomousExecutor({
|
|
977
|
+
workflowEngine: this.workflowEngine,
|
|
978
|
+
trustGate: this.trustGate,
|
|
979
|
+
trustEngine: this.trustEngine,
|
|
980
|
+
auditTrail: this.trustAuditTrail,
|
|
981
|
+
executeTool: async (name, params) => {
|
|
982
|
+
const context = {
|
|
983
|
+
sessionId: 'autonomous',
|
|
984
|
+
workingDirectory: getWorkspacePath(),
|
|
985
|
+
timeout: 30000,
|
|
986
|
+
};
|
|
987
|
+
return toolExecutor.execute(name, params, context);
|
|
988
|
+
},
|
|
989
|
+
onWorkflowCompleted: (workflowId) => {
|
|
990
|
+
const msg = `Autonomous workflow ${workflowId} completed`;
|
|
991
|
+
this.gateway.broadcast({
|
|
992
|
+
type: 'notification',
|
|
993
|
+
payload: { content: msg, system: true },
|
|
994
|
+
});
|
|
995
|
+
this.deliverToAllChannels(msg);
|
|
996
|
+
},
|
|
997
|
+
onStepFailed: (workflowId, stepId, error) => {
|
|
998
|
+
const msg = `Workflow ${workflowId} step ${stepId} failed: ${error}`;
|
|
999
|
+
this.gateway.broadcast({
|
|
1000
|
+
type: 'notification',
|
|
1001
|
+
payload: { content: msg, system: true },
|
|
1002
|
+
});
|
|
1003
|
+
this.deliverToAllChannels(msg);
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
this.autonomousExecutor.start(30_000);
|
|
1007
|
+
this.logger.info('Autonomous workflow executor started (30s tick)');
|
|
1008
|
+
}
|
|
1009
|
+
// Initialize connector registry and wire ambient scheduler
|
|
1010
|
+
this.connectorRegistry = new ConnectorRegistry();
|
|
1011
|
+
this.connectorAuthManager = new ConnectorAuthManager(this.vault);
|
|
1012
|
+
this.connectorRegistry.register(googleWorkspaceConnector);
|
|
1013
|
+
this.connectorRegistry.register(microsoftConnector);
|
|
1014
|
+
this.connectorRegistry.register(githubConnector);
|
|
1015
|
+
this.connectorRegistry.register(linearConnector);
|
|
1016
|
+
this.connectorRegistry.register(notionConnector);
|
|
1017
|
+
this.connectorRegistry.register(homeAssistantConnector);
|
|
1018
|
+
this.connectorRegistry.register(twitterConnector);
|
|
1019
|
+
this.connectorRegistry.register(linkedinConnector);
|
|
1020
|
+
this.connectorRegistry.register(redditConnector);
|
|
1021
|
+
this.connectorRegistry.register(instagramConnector);
|
|
1022
|
+
this.triggerManager = new TriggerManager(this.connectorRegistry, this.connectorAuthManager);
|
|
1023
|
+
// Restore connector tokens from vault (per-connector try-catch so one
|
|
1024
|
+
// failure doesn't prevent others from loading)
|
|
1025
|
+
for (const connector of this.connectorRegistry.list()) {
|
|
1026
|
+
try {
|
|
1027
|
+
const tokenData = this.vault.get(`connectors.${connector.id}.tokens`);
|
|
1028
|
+
if (tokenData) {
|
|
1029
|
+
const tokens = typeof tokenData === 'string' ? JSON.parse(tokenData) : tokenData;
|
|
1030
|
+
await this.connectorAuthManager.authenticate(connector.id, connector.auth, tokens);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
this.logger.warn(`Failed to restore tokens for connector ${connector.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// Register connector actions as AI-callable tools
|
|
1038
|
+
this.registerConnectorTools();
|
|
1039
|
+
// Wire ambient scheduler — always start it; connector-dependent features
|
|
1040
|
+
// (email/calendar) gracefully return empty when connectors aren't configured
|
|
1041
|
+
if (this.briefingGenerator) {
|
|
1042
|
+
const scheduler = new (await import('@auxiora/behaviors')).Scheduler();
|
|
1043
|
+
this.ambientScheduler = new AmbientScheduler({
|
|
1044
|
+
scheduler,
|
|
1045
|
+
connectorRegistry: this.connectorRegistry,
|
|
1046
|
+
triggerManager: this.triggerManager,
|
|
1047
|
+
briefingGenerator: this.briefingGenerator,
|
|
1048
|
+
notificationOrchestrator: this.notificationOrchestrator,
|
|
1049
|
+
deliveryChannel: async (msg) => {
|
|
1050
|
+
// Broadcast to all webchat connections
|
|
1051
|
+
this.gateway.broadcast({
|
|
1052
|
+
type: 'message',
|
|
1053
|
+
payload: { role: 'assistant', content: msg, system: true },
|
|
1054
|
+
});
|
|
1055
|
+
this.deliverToAllChannels(msg);
|
|
1056
|
+
},
|
|
1057
|
+
userId: 'default',
|
|
1058
|
+
config: DEFAULT_AMBIENT_SCHEDULER_CONFIG,
|
|
1059
|
+
});
|
|
1060
|
+
this.ambientScheduler.start();
|
|
1061
|
+
this.logger.info('Ambient scheduler started');
|
|
1062
|
+
}
|
|
1063
|
+
// [P15] Initialize conversation engine
|
|
1064
|
+
this.conversationEngine = new ConversationEngine();
|
|
1065
|
+
this.logger.info('Conversation engine initialized');
|
|
1066
|
+
// [P15] Initialize screen system (with mock backends — real backends injected at desktop layer)
|
|
1067
|
+
const mockCaptureBackend = {
|
|
1068
|
+
captureScreen: async () => ({ image: Buffer.alloc(0), timestamp: Date.now(), dimensions: { width: 0, height: 0 } }),
|
|
1069
|
+
captureRegion: async () => ({ image: Buffer.alloc(0), timestamp: Date.now(), dimensions: { width: 0, height: 0 } }),
|
|
1070
|
+
captureWindow: async () => ({ image: Buffer.alloc(0), timestamp: Date.now(), dimensions: { width: 0, height: 0 } }),
|
|
1071
|
+
};
|
|
1072
|
+
this.screenCapturer = new ScreenCapturer(mockCaptureBackend);
|
|
1073
|
+
this.logger.info('Screen system initialized (capture backend: mock)');
|
|
1074
|
+
// --- Self-awareness: capability catalog + health monitor ---
|
|
1075
|
+
const introspectionSources = {
|
|
1076
|
+
getTools: () => toolRegistry.list(),
|
|
1077
|
+
getConnectedChannels: () => this.channels?.getConnectedChannels() ?? [],
|
|
1078
|
+
getConfiguredChannels: () => this.channels?.getConfiguredChannels() ?? [],
|
|
1079
|
+
getDefaultChannelId: (type) => this.channels?.getDefaultChannelId(type),
|
|
1080
|
+
getBehaviors: async () => this.behaviors?.list() ?? [],
|
|
1081
|
+
getProviders: () => {
|
|
1082
|
+
if (!this.providers)
|
|
1083
|
+
return [];
|
|
1084
|
+
const names = this.providers.listAvailable();
|
|
1085
|
+
return names.map((n) => {
|
|
1086
|
+
const p = this.providers.getProvider(n);
|
|
1087
|
+
return { name: n, displayName: p.metadata.displayName, models: p.metadata.models };
|
|
1088
|
+
});
|
|
1089
|
+
},
|
|
1090
|
+
getPrimaryProviderName: () => this.config.provider.primary,
|
|
1091
|
+
getFallbackProviderName: () => this.config.provider.fallback,
|
|
1092
|
+
checkProviderAvailable: async (name) => {
|
|
1093
|
+
try {
|
|
1094
|
+
const p = this.providers?.getProvider(name);
|
|
1095
|
+
return p ? await p.metadata.isAvailable() : false;
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
},
|
|
1101
|
+
getPlugins: () => (this.pluginLoader?.listPlugins() ?? []).map((p) => ({
|
|
1102
|
+
name: p.name, version: p.version, status: 'active',
|
|
1103
|
+
toolCount: p.toolCount, behaviorNames: p.behaviorNames,
|
|
1104
|
+
})),
|
|
1105
|
+
getFeatures: () => ({
|
|
1106
|
+
behaviors: this.config.features?.behaviors !== false,
|
|
1107
|
+
browser: this.config.features?.browser !== false,
|
|
1108
|
+
voice: !!this.config.features?.voice,
|
|
1109
|
+
webhooks: !!this.config.features?.webhooks,
|
|
1110
|
+
plugins: !!this.config.features?.plugins,
|
|
1111
|
+
memory: this.config.memory?.enabled !== false,
|
|
1112
|
+
}),
|
|
1113
|
+
getAuditEntries: async (limit) => {
|
|
1114
|
+
const al = getAuditLogger();
|
|
1115
|
+
return al.getEntries(limit);
|
|
1116
|
+
},
|
|
1117
|
+
getTrustLevel: (domain) => this.trustEngine?.getTrustLevel(domain) ?? 0,
|
|
1118
|
+
};
|
|
1119
|
+
this.capabilityCatalog = new CapabilityCatalogImpl(introspectionSources);
|
|
1120
|
+
await this.capabilityCatalog.rebuild();
|
|
1121
|
+
const initialHealth = { overall: 'healthy', subsystems: [], issues: [], lastCheck: new Date().toISOString() };
|
|
1122
|
+
this.capabilityPromptFragment = generatePromptFragment(this.capabilityCatalog.getCatalog(), initialHealth, this.getSelfAwarenessContext());
|
|
1123
|
+
const autoFixActions = {
|
|
1124
|
+
reconnectChannel: async () => false,
|
|
1125
|
+
restartBehavior: async (_id) => {
|
|
1126
|
+
// BehaviorManager does not yet expose a resume() method
|
|
1127
|
+
return false;
|
|
1128
|
+
},
|
|
1129
|
+
};
|
|
1130
|
+
this.healthMonitor = new HealthMonitorImpl(introspectionSources, autoFixActions);
|
|
1131
|
+
this.healthMonitor.onChange((state) => {
|
|
1132
|
+
this.capabilityPromptFragment = generatePromptFragment(this.capabilityCatalog.getCatalog(), state, this.getSelfAwarenessContext());
|
|
1133
|
+
this.gateway.broadcast({ type: 'health_update', payload: state }, (client) => client.authenticated);
|
|
1134
|
+
});
|
|
1135
|
+
this.healthMonitor.start(30_000);
|
|
1136
|
+
const introspectTool = createIntrospectTool(() => this.capabilityCatalog.getCatalog(), () => this.healthMonitor.getHealthState(), introspectionSources);
|
|
1137
|
+
toolRegistry.register(introspectTool);
|
|
1138
|
+
// Update catalog on relevant audit events
|
|
1139
|
+
const introspectionAuditLogger = getAuditLogger();
|
|
1140
|
+
const prevOnEntry = introspectionAuditLogger.onEntry;
|
|
1141
|
+
introspectionAuditLogger.onEntry = (entry) => {
|
|
1142
|
+
prevOnEntry?.(entry);
|
|
1143
|
+
if (entry.event.startsWith('channel.') || entry.event.startsWith('plugin.')) {
|
|
1144
|
+
this.capabilityCatalog?.rebuildSection(entry.event.startsWith('channel.') ? 'channels' : 'plugins');
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
this.logger.info('Self-awareness initialized', {
|
|
1148
|
+
tools: this.capabilityCatalog.getCatalog().tools.length,
|
|
1149
|
+
channels: this.capabilityCatalog.getCatalog().channels.length,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
async initializeProviders() {
|
|
1153
|
+
let anthropicKey;
|
|
1154
|
+
let anthropicOAuthToken;
|
|
1155
|
+
let openaiKey;
|
|
1156
|
+
let googleKey;
|
|
1157
|
+
let groqKey;
|
|
1158
|
+
let deepseekKey;
|
|
1159
|
+
let cohereKey;
|
|
1160
|
+
let xaiKey;
|
|
1161
|
+
let replicateToken;
|
|
1162
|
+
let vaultLocked = false;
|
|
1163
|
+
try {
|
|
1164
|
+
anthropicKey = this.vault.get('ANTHROPIC_API_KEY');
|
|
1165
|
+
anthropicOAuthToken = this.vault.get('ANTHROPIC_OAUTH_TOKEN');
|
|
1166
|
+
openaiKey = this.vault.get('OPENAI_API_KEY');
|
|
1167
|
+
googleKey = this.vault.get('GOOGLE_API_KEY');
|
|
1168
|
+
groqKey = this.vault.get('GROQ_API_KEY');
|
|
1169
|
+
deepseekKey = this.vault.get('DEEPSEEK_API_KEY');
|
|
1170
|
+
cohereKey = this.vault.get('COHERE_API_KEY');
|
|
1171
|
+
xaiKey = this.vault.get('XAI_API_KEY');
|
|
1172
|
+
replicateToken = this.vault.get('REPLICATE_API_TOKEN');
|
|
1173
|
+
// Check if ANTHROPIC_API_KEY is actually an OAuth token (sk-ant-oat01-*)
|
|
1174
|
+
// This handles users who stored their OAuth token in the wrong vault key
|
|
1175
|
+
if (anthropicKey && isSetupToken(anthropicKey)) {
|
|
1176
|
+
anthropicOAuthToken = anthropicKey;
|
|
1177
|
+
anthropicKey = undefined;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
vaultLocked = true;
|
|
1182
|
+
}
|
|
1183
|
+
// Initialize media processor with auto-detected providers
|
|
1184
|
+
// detectProviders() calls vault.get() — guard against locked vault
|
|
1185
|
+
try {
|
|
1186
|
+
this.mediaProcessor = new MediaProcessor(detectProviders(this.vault));
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
this.mediaProcessor = new MediaProcessor([]);
|
|
1190
|
+
}
|
|
1191
|
+
// Check for Claude CLI credentials as fallback
|
|
1192
|
+
const cliCreds = readClaudeCliCredentials();
|
|
1193
|
+
const hasCliCredentials = cliCreds !== null;
|
|
1194
|
+
const hasAnthropic = anthropicKey || anthropicOAuthToken || hasCliCredentials;
|
|
1195
|
+
const hasOllama = this.config.provider.ollama?.model;
|
|
1196
|
+
const hasAnyKey = hasAnthropic || openaiKey || googleKey || groqKey || deepseekKey || cohereKey || xaiKey || replicateToken || hasOllama;
|
|
1197
|
+
if (!hasAnyKey) {
|
|
1198
|
+
if (vaultLocked) {
|
|
1199
|
+
this.logger.warn('Vault is locked. AI providers not initialized.');
|
|
1200
|
+
this.logger.warn('To use AI: auxiora vault add ANTHROPIC_API_KEY');
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
this.logger.warn('No API keys found in vault. Add with: auxiora vault add ANTHROPIC_API_KEY');
|
|
1204
|
+
this.logger.warn('Or for Claude Pro/Max OAuth: auxiora vault add ANTHROPIC_OAUTH_TOKEN');
|
|
1205
|
+
this.logger.warn('Or authenticate with: claude setup-token');
|
|
1206
|
+
}
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
// Build Anthropic config - prefer vault credentials, fall back to CLI
|
|
1210
|
+
let anthropicConfig;
|
|
1211
|
+
if (anthropicOAuthToken) {
|
|
1212
|
+
const tokenPrefix = anthropicOAuthToken.substring(0, 15);
|
|
1213
|
+
this.logger.info(`Using Anthropic OAuth token from vault (${tokenPrefix}...)`);
|
|
1214
|
+
const vault = this.vault;
|
|
1215
|
+
const expiresAtStr = vault.get('CLAUDE_OAUTH_EXPIRES_AT');
|
|
1216
|
+
const tokenExpiresAt = expiresAtStr ? Number(expiresAtStr) : undefined;
|
|
1217
|
+
if (tokenExpiresAt) {
|
|
1218
|
+
const minutesLeft = Math.round((tokenExpiresAt - Date.now()) / 60000);
|
|
1219
|
+
this.logger.info(`OAuth token expires in ${minutesLeft} minutes`);
|
|
1220
|
+
}
|
|
1221
|
+
anthropicConfig = {
|
|
1222
|
+
oauthToken: anthropicOAuthToken,
|
|
1223
|
+
tokenExpiresAt,
|
|
1224
|
+
model: this.config.provider.anthropic.model,
|
|
1225
|
+
maxTokens: this.config.provider.anthropic.maxTokens,
|
|
1226
|
+
onTokenRefresh: async () => {
|
|
1227
|
+
const rt = vault.get('CLAUDE_OAUTH_REFRESH_TOKEN');
|
|
1228
|
+
if (!rt) {
|
|
1229
|
+
this.logger.warn('No refresh token in vault — cannot auto-refresh. Re-authenticate via Dashboard > Settings > Provider.');
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
this.logger.info('Attempting OAuth token refresh...');
|
|
1233
|
+
// Try PKCE client first (dashboard OAuth flow), then CLI client
|
|
1234
|
+
const methods = [
|
|
1235
|
+
{ name: 'PKCE', fn: refreshPKCEOAuthToken },
|
|
1236
|
+
{ name: 'CLI', fn: refreshOAuthToken },
|
|
1237
|
+
];
|
|
1238
|
+
for (const method of methods) {
|
|
1239
|
+
try {
|
|
1240
|
+
const refreshed = await method.fn(rt);
|
|
1241
|
+
await vault.add('ANTHROPIC_OAUTH_TOKEN', refreshed.accessToken);
|
|
1242
|
+
await vault.add('CLAUDE_OAUTH_REFRESH_TOKEN', refreshed.refreshToken);
|
|
1243
|
+
await vault.add('CLAUDE_OAUTH_EXPIRES_AT', String(refreshed.expiresAt));
|
|
1244
|
+
this.logger.info(`OAuth token refreshed via ${method.name} client`);
|
|
1245
|
+
return refreshed.accessToken;
|
|
1246
|
+
}
|
|
1247
|
+
catch (err) {
|
|
1248
|
+
this.logger.warn(`${method.name} token refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
this.logger.error('All token refresh methods failed. Re-authenticate via Dashboard > Settings > Provider.', { error: new Error('Token refresh exhausted') });
|
|
1252
|
+
return null;
|
|
1253
|
+
},
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
else if (anthropicKey) {
|
|
1257
|
+
this.logger.info('Using Anthropic API key from vault');
|
|
1258
|
+
anthropicConfig = {
|
|
1259
|
+
apiKey: anthropicKey,
|
|
1260
|
+
model: this.config.provider.anthropic.model,
|
|
1261
|
+
maxTokens: this.config.provider.anthropic.maxTokens,
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
else if (hasCliCredentials) {
|
|
1265
|
+
this.logger.info('Using Claude CLI credentials (~/.claude/.credentials.json)');
|
|
1266
|
+
anthropicConfig = {
|
|
1267
|
+
useCliCredentials: true,
|
|
1268
|
+
model: this.config.provider.anthropic.model,
|
|
1269
|
+
maxTokens: this.config.provider.anthropic.maxTokens,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
this.providers = new ProviderFactory({
|
|
1273
|
+
primary: this.config.provider.primary,
|
|
1274
|
+
fallback: this.config.provider.fallback,
|
|
1275
|
+
config: {
|
|
1276
|
+
anthropic: anthropicConfig,
|
|
1277
|
+
openai: openaiKey
|
|
1278
|
+
? {
|
|
1279
|
+
apiKey: openaiKey,
|
|
1280
|
+
model: this.config.provider.openai.model,
|
|
1281
|
+
maxTokens: this.config.provider.openai.maxTokens,
|
|
1282
|
+
}
|
|
1283
|
+
: undefined,
|
|
1284
|
+
google: googleKey
|
|
1285
|
+
? {
|
|
1286
|
+
apiKey: googleKey,
|
|
1287
|
+
model: this.config.provider.google.model,
|
|
1288
|
+
maxTokens: this.config.provider.google.maxTokens,
|
|
1289
|
+
}
|
|
1290
|
+
: undefined,
|
|
1291
|
+
ollama: {
|
|
1292
|
+
baseUrl: this.config.provider.ollama.baseUrl,
|
|
1293
|
+
model: this.config.provider.ollama.model,
|
|
1294
|
+
maxTokens: this.config.provider.ollama.maxTokens,
|
|
1295
|
+
},
|
|
1296
|
+
openaiCompatible: this.config.provider.openaiCompatible.baseUrl
|
|
1297
|
+
? {
|
|
1298
|
+
baseUrl: this.config.provider.openaiCompatible.baseUrl,
|
|
1299
|
+
model: this.config.provider.openaiCompatible.model,
|
|
1300
|
+
maxTokens: this.config.provider.openaiCompatible.maxTokens,
|
|
1301
|
+
name: this.config.provider.openaiCompatible.name,
|
|
1302
|
+
}
|
|
1303
|
+
: undefined,
|
|
1304
|
+
groq: groqKey
|
|
1305
|
+
? {
|
|
1306
|
+
apiKey: groqKey,
|
|
1307
|
+
model: this.config.provider.groq.model,
|
|
1308
|
+
maxTokens: this.config.provider.groq.maxTokens,
|
|
1309
|
+
}
|
|
1310
|
+
: undefined,
|
|
1311
|
+
deepseek: deepseekKey
|
|
1312
|
+
? {
|
|
1313
|
+
apiKey: deepseekKey,
|
|
1314
|
+
model: this.config.provider.deepseek.model,
|
|
1315
|
+
maxTokens: this.config.provider.deepseek.maxTokens,
|
|
1316
|
+
}
|
|
1317
|
+
: undefined,
|
|
1318
|
+
cohere: cohereKey
|
|
1319
|
+
? {
|
|
1320
|
+
apiKey: cohereKey,
|
|
1321
|
+
model: this.config.provider.cohere.model,
|
|
1322
|
+
maxTokens: this.config.provider.cohere.maxTokens,
|
|
1323
|
+
}
|
|
1324
|
+
: undefined,
|
|
1325
|
+
xai: xaiKey
|
|
1326
|
+
? {
|
|
1327
|
+
apiKey: xaiKey,
|
|
1328
|
+
model: this.config.provider.xai.model,
|
|
1329
|
+
maxTokens: this.config.provider.xai.maxTokens,
|
|
1330
|
+
}
|
|
1331
|
+
: undefined,
|
|
1332
|
+
replicate: replicateToken
|
|
1333
|
+
? {
|
|
1334
|
+
apiToken: replicateToken,
|
|
1335
|
+
model: this.config.provider.replicate.model,
|
|
1336
|
+
}
|
|
1337
|
+
: undefined,
|
|
1338
|
+
},
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
initializeRouter() {
|
|
1342
|
+
// Gather provider metadata from available providers
|
|
1343
|
+
const availableProviders = new Map();
|
|
1344
|
+
for (const name of this.providers.listAvailable()) {
|
|
1345
|
+
const provider = this.providers.getProvider(name);
|
|
1346
|
+
if (provider.metadata) {
|
|
1347
|
+
availableProviders.set(name, provider.metadata);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (availableProviders.size === 0)
|
|
1351
|
+
return;
|
|
1352
|
+
const classifier = new TaskClassifier();
|
|
1353
|
+
const selector = new ModelSelector(availableProviders, this.config.routing);
|
|
1354
|
+
const costTracker = new CostTracker(this.config.routing.costLimits);
|
|
1355
|
+
this.modelRouter = new ModelRouter(classifier, selector, costTracker, availableProviders);
|
|
1356
|
+
this.logger.info(`Model router initialized with ${availableProviders.size} provider(s)`);
|
|
1357
|
+
}
|
|
1358
|
+
getRouter() {
|
|
1359
|
+
return this.modelRouter;
|
|
1360
|
+
}
|
|
1361
|
+
getOrchestrationEngine() {
|
|
1362
|
+
return this.orchestrationEngine;
|
|
1363
|
+
}
|
|
1364
|
+
getCostSummary() {
|
|
1365
|
+
return this.modelRouter?.getCostSummary();
|
|
1366
|
+
}
|
|
1367
|
+
async initializeChannels() {
|
|
1368
|
+
// Get channel tokens from vault
|
|
1369
|
+
let discordToken;
|
|
1370
|
+
let telegramToken;
|
|
1371
|
+
let slackBotToken;
|
|
1372
|
+
let slackAppToken;
|
|
1373
|
+
let twilioAccountSid;
|
|
1374
|
+
let twilioAuthToken;
|
|
1375
|
+
let twilioPhoneNumber;
|
|
1376
|
+
let emailAddress;
|
|
1377
|
+
let emailPassword;
|
|
1378
|
+
let emailImapHost;
|
|
1379
|
+
let emailImapPort;
|
|
1380
|
+
let emailSmtpHost;
|
|
1381
|
+
let emailSmtpPort;
|
|
1382
|
+
let matrixAccessToken;
|
|
1383
|
+
let matrixHomeserverUrl;
|
|
1384
|
+
let matrixUserId;
|
|
1385
|
+
let signalCliEndpoint;
|
|
1386
|
+
let signalPhoneNumber;
|
|
1387
|
+
let teamsAppId;
|
|
1388
|
+
let teamsAppPassword;
|
|
1389
|
+
let whatsappPhoneNumberId;
|
|
1390
|
+
let whatsappAccessToken;
|
|
1391
|
+
let whatsappVerifyToken;
|
|
1392
|
+
try {
|
|
1393
|
+
discordToken = this.vault.get('DISCORD_BOT_TOKEN');
|
|
1394
|
+
telegramToken = this.vault.get('TELEGRAM_BOT_TOKEN');
|
|
1395
|
+
slackBotToken = this.vault.get('SLACK_BOT_TOKEN');
|
|
1396
|
+
slackAppToken = this.vault.get('SLACK_APP_TOKEN');
|
|
1397
|
+
twilioAccountSid = this.vault.get('TWILIO_ACCOUNT_SID');
|
|
1398
|
+
twilioAuthToken = this.vault.get('TWILIO_AUTH_TOKEN');
|
|
1399
|
+
twilioPhoneNumber = this.vault.get('TWILIO_PHONE_NUMBER');
|
|
1400
|
+
emailAddress = this.vault.get('EMAIL_ADDRESS');
|
|
1401
|
+
emailPassword = this.vault.get('EMAIL_PASSWORD');
|
|
1402
|
+
emailImapHost = this.vault.get('EMAIL_IMAP_HOST');
|
|
1403
|
+
emailImapPort = this.vault.get('EMAIL_IMAP_PORT');
|
|
1404
|
+
emailSmtpHost = this.vault.get('EMAIL_SMTP_HOST');
|
|
1405
|
+
emailSmtpPort = this.vault.get('EMAIL_SMTP_PORT');
|
|
1406
|
+
matrixAccessToken = this.vault.get('MATRIX_ACCESS_TOKEN');
|
|
1407
|
+
matrixHomeserverUrl = this.vault.get('MATRIX_HOMESERVER_URL');
|
|
1408
|
+
matrixUserId = this.vault.get('MATRIX_USER_ID');
|
|
1409
|
+
signalCliEndpoint = this.vault.get('SIGNAL_CLI_ENDPOINT');
|
|
1410
|
+
signalPhoneNumber = this.vault.get('SIGNAL_PHONE_NUMBER');
|
|
1411
|
+
teamsAppId = this.vault.get('TEAMS_APP_ID');
|
|
1412
|
+
teamsAppPassword = this.vault.get('TEAMS_APP_PASSWORD');
|
|
1413
|
+
whatsappPhoneNumberId = this.vault.get('WHATSAPP_PHONE_NUMBER_ID');
|
|
1414
|
+
whatsappAccessToken = this.vault.get('WHATSAPP_ACCESS_TOKEN');
|
|
1415
|
+
whatsappVerifyToken = this.vault.get('WHATSAPP_VERIFY_TOKEN');
|
|
1416
|
+
}
|
|
1417
|
+
catch {
|
|
1418
|
+
// Vault is locked
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const hasAnyChannel = (this.config.channels.discord.enabled && discordToken) ||
|
|
1422
|
+
(this.config.channels.telegram.enabled && telegramToken) ||
|
|
1423
|
+
(this.config.channels.slack.enabled && slackBotToken && slackAppToken) ||
|
|
1424
|
+
(this.config.channels.twilio.enabled && twilioAccountSid && twilioAuthToken) ||
|
|
1425
|
+
(this.config.channels.email.enabled && emailAddress && emailPassword && emailImapHost && emailSmtpHost) ||
|
|
1426
|
+
(this.config.channels.matrix.enabled && matrixAccessToken && matrixHomeserverUrl) ||
|
|
1427
|
+
(this.config.channels.signal.enabled && signalCliEndpoint && signalPhoneNumber) ||
|
|
1428
|
+
(this.config.channels.teams.enabled && teamsAppId && teamsAppPassword) ||
|
|
1429
|
+
(this.config.channels.whatsapp.enabled && whatsappPhoneNumberId && whatsappAccessToken && whatsappVerifyToken);
|
|
1430
|
+
if (!hasAnyChannel) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
this.channels = new ChannelManager({
|
|
1434
|
+
discord: this.config.channels.discord.enabled && discordToken
|
|
1435
|
+
? {
|
|
1436
|
+
token: discordToken,
|
|
1437
|
+
mentionOnly: this.config.channels.discord.mentionOnly,
|
|
1438
|
+
}
|
|
1439
|
+
: undefined,
|
|
1440
|
+
telegram: this.config.channels.telegram.enabled && telegramToken
|
|
1441
|
+
? {
|
|
1442
|
+
token: telegramToken,
|
|
1443
|
+
}
|
|
1444
|
+
: undefined,
|
|
1445
|
+
slack: this.config.channels.slack.enabled && slackBotToken && slackAppToken
|
|
1446
|
+
? {
|
|
1447
|
+
botToken: slackBotToken,
|
|
1448
|
+
appToken: slackAppToken,
|
|
1449
|
+
}
|
|
1450
|
+
: undefined,
|
|
1451
|
+
twilio: this.config.channels.twilio.enabled && twilioAccountSid && twilioAuthToken && twilioPhoneNumber
|
|
1452
|
+
? {
|
|
1453
|
+
accountSid: twilioAccountSid,
|
|
1454
|
+
authToken: twilioAuthToken,
|
|
1455
|
+
phoneNumber: twilioPhoneNumber,
|
|
1456
|
+
}
|
|
1457
|
+
: undefined,
|
|
1458
|
+
email: this.config.channels.email.enabled && emailAddress && emailPassword && emailImapHost && emailSmtpHost
|
|
1459
|
+
? {
|
|
1460
|
+
imapHost: emailImapHost,
|
|
1461
|
+
imapPort: Number(emailImapPort) || 993,
|
|
1462
|
+
smtpHost: emailSmtpHost,
|
|
1463
|
+
smtpPort: Number(emailSmtpPort) || 465,
|
|
1464
|
+
email: emailAddress,
|
|
1465
|
+
password: emailPassword,
|
|
1466
|
+
pollInterval: this.config.channels.email.pollInterval,
|
|
1467
|
+
}
|
|
1468
|
+
: undefined,
|
|
1469
|
+
matrix: this.config.channels.matrix.enabled && matrixAccessToken && matrixHomeserverUrl && matrixUserId
|
|
1470
|
+
? {
|
|
1471
|
+
homeserverUrl: matrixHomeserverUrl,
|
|
1472
|
+
userId: matrixUserId,
|
|
1473
|
+
accessToken: matrixAccessToken,
|
|
1474
|
+
autoJoinRooms: this.config.channels.matrix.autoJoinRooms,
|
|
1475
|
+
}
|
|
1476
|
+
: undefined,
|
|
1477
|
+
signal: this.config.channels.signal.enabled && signalCliEndpoint && signalPhoneNumber
|
|
1478
|
+
? {
|
|
1479
|
+
signalCliEndpoint,
|
|
1480
|
+
phoneNumber: signalPhoneNumber,
|
|
1481
|
+
}
|
|
1482
|
+
: undefined,
|
|
1483
|
+
teams: this.config.channels.teams.enabled && teamsAppId && teamsAppPassword
|
|
1484
|
+
? {
|
|
1485
|
+
microsoftAppId: teamsAppId,
|
|
1486
|
+
microsoftAppPassword: teamsAppPassword,
|
|
1487
|
+
}
|
|
1488
|
+
: undefined,
|
|
1489
|
+
whatsapp: this.config.channels.whatsapp.enabled && whatsappPhoneNumberId && whatsappAccessToken && whatsappVerifyToken
|
|
1490
|
+
? {
|
|
1491
|
+
phoneNumberId: whatsappPhoneNumberId,
|
|
1492
|
+
accessToken: whatsappAccessToken,
|
|
1493
|
+
verifyToken: whatsappVerifyToken,
|
|
1494
|
+
}
|
|
1495
|
+
: undefined,
|
|
1496
|
+
});
|
|
1497
|
+
// Set up channel message handler
|
|
1498
|
+
this.channels.onMessage(this.handleChannelMessage.bind(this));
|
|
1499
|
+
this.channels.onError((error, channelType) => {
|
|
1500
|
+
this.logger.error('Channel error', { channelType, error: new Error(error.message) });
|
|
1501
|
+
});
|
|
1502
|
+
if (this.pluginLoader) {
|
|
1503
|
+
this.pluginLoader.setChannelManager(this.channels);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
buildIdentityPreamble(agent) {
|
|
1507
|
+
const lines = ['# Agent Identity'];
|
|
1508
|
+
lines.push(`You are ${agent.name} (${agent.pronouns}).`);
|
|
1509
|
+
lines.push('');
|
|
1510
|
+
lines.push('## Personality');
|
|
1511
|
+
lines.push(`Warmth: ${agent.tone.warmth}/1.0 | Directness: ${agent.tone.directness}/1.0 | Humor: ${agent.tone.humor}/1.0 | Formality: ${agent.tone.formality}/1.0`);
|
|
1512
|
+
lines.push(`Error handling style: ${agent.errorStyle}`);
|
|
1513
|
+
if (agent.expertise.length > 0) {
|
|
1514
|
+
lines.push('');
|
|
1515
|
+
lines.push('## Expertise');
|
|
1516
|
+
for (const area of agent.expertise) {
|
|
1517
|
+
lines.push(`- ${area}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
const phrases = Object.entries(agent.catchphrases).filter(([, v]) => v);
|
|
1521
|
+
if (phrases.length > 0) {
|
|
1522
|
+
lines.push('');
|
|
1523
|
+
lines.push('## Catchphrases');
|
|
1524
|
+
for (const [key, value] of phrases) {
|
|
1525
|
+
lines.push(`- ${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
const hasJokeBoundaries = agent.boundaries.neverJokeAbout.length > 0;
|
|
1529
|
+
const hasAdviseBoundaries = agent.boundaries.neverAdviseOn.length > 0;
|
|
1530
|
+
if (hasJokeBoundaries || hasAdviseBoundaries) {
|
|
1531
|
+
lines.push('');
|
|
1532
|
+
lines.push('## Boundaries');
|
|
1533
|
+
if (hasJokeBoundaries) {
|
|
1534
|
+
lines.push(`Never joke about: ${agent.boundaries.neverJokeAbout.join(', ')}`);
|
|
1535
|
+
}
|
|
1536
|
+
if (hasAdviseBoundaries) {
|
|
1537
|
+
lines.push(`Never advise on: ${agent.boundaries.neverAdviseOn.join(', ')}`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return lines.join('\n');
|
|
1541
|
+
}
|
|
1542
|
+
async loadArchitectPersonality() {
|
|
1543
|
+
// Identity preamble first so the AI knows its configured name (e.g. "Aria")
|
|
1544
|
+
const agent = this.config.agent;
|
|
1545
|
+
const parts = [this.buildIdentityPreamble(agent), ARCHITECT_BASE_PROMPT];
|
|
1546
|
+
// Workspace files still provide user context
|
|
1547
|
+
try {
|
|
1548
|
+
parts.push(await fs.readFile(getAgentsPath(), 'utf-8'));
|
|
1549
|
+
}
|
|
1550
|
+
catch { /* no file */ }
|
|
1551
|
+
try {
|
|
1552
|
+
parts.push(await fs.readFile(getIdentityPath(), 'utf-8'));
|
|
1553
|
+
}
|
|
1554
|
+
catch { /* no file */ }
|
|
1555
|
+
try {
|
|
1556
|
+
const user = await fs.readFile(getUserPath(), 'utf-8');
|
|
1557
|
+
parts.push(`\n## About the User\n${user}`);
|
|
1558
|
+
}
|
|
1559
|
+
catch { /* no file */ }
|
|
1560
|
+
this.architectPrompt = parts.join('\n\n---\n\n');
|
|
1561
|
+
const storage = new VaultStorageAdapter(this.vault);
|
|
1562
|
+
this.architect = createArchitect(storage);
|
|
1563
|
+
await this.architect.initialize();
|
|
1564
|
+
if (this.capabilityPromptFragment) {
|
|
1565
|
+
this.architectPrompt += '\n\n---\n\n' + this.capabilityPromptFragment;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async loadPersonality() {
|
|
1569
|
+
// Always initialize Architect engine (it's lightweight) so any chat can use it.
|
|
1570
|
+
// Wrap in try/catch: vault may be locked during setup mode.
|
|
1571
|
+
try {
|
|
1572
|
+
await this.loadArchitectPersonality();
|
|
1573
|
+
}
|
|
1574
|
+
catch {
|
|
1575
|
+
this.logger.warn('Architect personality not available (vault may be locked)');
|
|
1576
|
+
}
|
|
1577
|
+
// Initialize Architect bridge for state persistence and awareness bridging
|
|
1578
|
+
if (this.architect) {
|
|
1579
|
+
this.architectAwarenessCollector = new ArchitectAwarenessCollector();
|
|
1580
|
+
this.architectBridge = new ArchitectBridge(this.architect, this.architectAwarenessCollector, this.vault, {
|
|
1581
|
+
onEscalation: (alert, context) => {
|
|
1582
|
+
this.logger.warn('Escalation detected', {
|
|
1583
|
+
alert,
|
|
1584
|
+
domain: context.domain,
|
|
1585
|
+
emotion: context.emotionalRegister,
|
|
1586
|
+
});
|
|
1587
|
+
},
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
// Build standard prompt
|
|
1591
|
+
const parts = [];
|
|
1592
|
+
// Build identity preamble from config
|
|
1593
|
+
const agent = this.config.agent;
|
|
1594
|
+
parts.push(this.buildIdentityPreamble(agent));
|
|
1595
|
+
// Add personality adaptations from living memory
|
|
1596
|
+
if (this.personalityAdapter) {
|
|
1597
|
+
const modifier = await this.personalityAdapter.getPromptModifier();
|
|
1598
|
+
if (modifier) {
|
|
1599
|
+
parts.push(modifier);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
// Load SOUL.md
|
|
1603
|
+
let soulContent;
|
|
1604
|
+
try {
|
|
1605
|
+
soulContent = await fs.readFile(getSoulPath(), 'utf-8');
|
|
1606
|
+
parts.push(soulContent);
|
|
1607
|
+
}
|
|
1608
|
+
catch {
|
|
1609
|
+
// No SOUL.md
|
|
1610
|
+
}
|
|
1611
|
+
// Apply SOUL.md domain biases to Architect trait mixing
|
|
1612
|
+
if (this.architect && soulContent) {
|
|
1613
|
+
const biases = parseSoulBiases(soulContent);
|
|
1614
|
+
for (const [trait, offset] of Object.entries(biases)) {
|
|
1615
|
+
this.architect.setTraitOverride(trait, offset).catch(() => { });
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
// Load AGENTS.md
|
|
1619
|
+
try {
|
|
1620
|
+
const agents = await fs.readFile(getAgentsPath(), 'utf-8');
|
|
1621
|
+
parts.push(agents);
|
|
1622
|
+
}
|
|
1623
|
+
catch {
|
|
1624
|
+
// No AGENTS.md
|
|
1625
|
+
}
|
|
1626
|
+
// Load IDENTITY.md
|
|
1627
|
+
try {
|
|
1628
|
+
const identity = await fs.readFile(getIdentityPath(), 'utf-8');
|
|
1629
|
+
parts.push(identity);
|
|
1630
|
+
}
|
|
1631
|
+
catch {
|
|
1632
|
+
// No IDENTITY.md
|
|
1633
|
+
}
|
|
1634
|
+
// Load USER.md
|
|
1635
|
+
try {
|
|
1636
|
+
const user = await fs.readFile(getUserPath(), 'utf-8');
|
|
1637
|
+
parts.push(`\n## About the User\n${user}`);
|
|
1638
|
+
}
|
|
1639
|
+
catch {
|
|
1640
|
+
// No USER.md
|
|
1641
|
+
}
|
|
1642
|
+
if (parts.length > 1) {
|
|
1643
|
+
// Has content beyond the identity preamble
|
|
1644
|
+
this.standardPrompt = parts.join('\n\n---\n\n');
|
|
1645
|
+
}
|
|
1646
|
+
else {
|
|
1647
|
+
// Only identity preamble, no personality files — use enriched default
|
|
1648
|
+
this.standardPrompt = `You are ${agent.name}, a helpful AI assistant. Be concise, accurate, and friendly.`;
|
|
1649
|
+
}
|
|
1650
|
+
// Append tool usage guidance
|
|
1651
|
+
this.standardPrompt += '\n\n---\n\n## Tool Usage\n'
|
|
1652
|
+
+ '- For reading web pages, searching, fetching articles, or looking up information, use the `web_browser` tool. It is fast, lightweight, and always available.\n'
|
|
1653
|
+
+ '- Only use `browser_navigate` and other browser_* tools when you need JavaScript rendering or interactive features (clicking buttons, filling forms, taking screenshots).\n'
|
|
1654
|
+
+ '- Never expose raw tool errors to the user. If a tool fails, explain the situation naturally.';
|
|
1655
|
+
// Append self-awareness capability fragment
|
|
1656
|
+
if (this.capabilityPromptFragment) {
|
|
1657
|
+
this.standardPrompt += '\n\n---\n\n' + this.capabilityPromptFragment;
|
|
1658
|
+
}
|
|
1659
|
+
// Set global system prompt based on config
|
|
1660
|
+
this.systemPrompt = this.config.agent.personality === 'the-architect'
|
|
1661
|
+
? this.architectPrompt
|
|
1662
|
+
: this.standardPrompt;
|
|
1663
|
+
// Initialize dynamic self-awareness assembler
|
|
1664
|
+
if (this.config.selfAwareness?.enabled) {
|
|
1665
|
+
const storage = new InMemoryAwarenessStorage();
|
|
1666
|
+
const collectorConfig = this.config.selfAwareness.collectors ?? {};
|
|
1667
|
+
const collectors = [
|
|
1668
|
+
...(collectorConfig.conversationReflector !== false ? [new ConversationReflector(storage)] : []),
|
|
1669
|
+
...(collectorConfig.capacityMonitor !== false ? [new CapacityMonitor()] : []),
|
|
1670
|
+
...(collectorConfig.knowledgeBoundary !== false ? [new KnowledgeBoundary(storage)] : []),
|
|
1671
|
+
...(collectorConfig.relationshipModel !== false ? [new RelationshipModel(storage)] : []),
|
|
1672
|
+
...(collectorConfig.temporalTracker !== false ? [new TemporalTracker(storage)] : []),
|
|
1673
|
+
...(collectorConfig.environmentSensor !== false ? [new EnvironmentSensor()] : []),
|
|
1674
|
+
...(collectorConfig.metaCognitor !== false ? [new MetaCognitor(storage)] : []),
|
|
1675
|
+
];
|
|
1676
|
+
if (this.architectAwarenessCollector) {
|
|
1677
|
+
collectors.push(this.architectAwarenessCollector);
|
|
1678
|
+
}
|
|
1679
|
+
this.selfAwarenessAssembler = new SelfAwarenessAssembler(collectors, {
|
|
1680
|
+
tokenBudget: this.config.selfAwareness.tokenBudget ?? 500,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
/** Switch the global personality engine at runtime (no restart required). */
|
|
1685
|
+
setPersonalityEngine(engine) {
|
|
1686
|
+
this.config.agent.personality = engine;
|
|
1687
|
+
this.systemPrompt = engine === 'the-architect'
|
|
1688
|
+
? this.architectPrompt
|
|
1689
|
+
: this.standardPrompt;
|
|
1690
|
+
// Rebuild self-awareness fragment so the AI knows its personality changed
|
|
1691
|
+
if (this.capabilityCatalog && this.healthMonitor) {
|
|
1692
|
+
this.capabilityPromptFragment = generatePromptFragment(this.capabilityCatalog.getCatalog(), this.healthMonitor.getHealthState(), this.getSelfAwarenessContext());
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
getSelfAwarenessContext() {
|
|
1696
|
+
const primary = this.config.provider.primary;
|
|
1697
|
+
// Read model directly from the live provider instance — this is the actual
|
|
1698
|
+
// model being used, not just what the config file says.
|
|
1699
|
+
const provider = this.providers.getPrimaryProvider();
|
|
1700
|
+
return {
|
|
1701
|
+
defaultModel: provider.defaultModel,
|
|
1702
|
+
primaryProvider: primary,
|
|
1703
|
+
personalityEngine: this.config.agent.personality ?? 'standard',
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
/** Append Architect context modifier when active, returning context metadata. */
|
|
1707
|
+
applyArchitectEnrichment(prompt, userMessage, chatId) {
|
|
1708
|
+
if (!this.architect)
|
|
1709
|
+
return { prompt };
|
|
1710
|
+
const output = this.architect.generatePrompt(userMessage);
|
|
1711
|
+
// Bridge handles side effects: persistence, awareness feeding, escalation logging
|
|
1712
|
+
if (this.architectBridge && chatId) {
|
|
1713
|
+
this.architectBridge.afterPrompt(output.detectedContext, output.emotionalTrajectory, output.escalationAlert, chatId);
|
|
1714
|
+
}
|
|
1715
|
+
const mix = this.architect.getTraitMix(output.detectedContext);
|
|
1716
|
+
const traitWeights = {};
|
|
1717
|
+
for (const [key, val] of Object.entries(mix)) {
|
|
1718
|
+
traitWeights[key] = val;
|
|
1719
|
+
}
|
|
1720
|
+
return {
|
|
1721
|
+
prompt: prompt + '\n\n' + output.contextModifier,
|
|
1722
|
+
architectMeta: {
|
|
1723
|
+
detectedContext: output.detectedContext,
|
|
1724
|
+
activeTraits: output.activeTraits,
|
|
1725
|
+
traitWeights,
|
|
1726
|
+
recommendation: output.recommendation,
|
|
1727
|
+
escalationAlert: output.escalationAlert,
|
|
1728
|
+
},
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
async initializeModes() {
|
|
1732
|
+
if (this.config.modes?.enabled === false)
|
|
1733
|
+
return;
|
|
1734
|
+
const builtInModesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../personality/modes');
|
|
1735
|
+
const userModesDir = getModesDir();
|
|
1736
|
+
this.modeLoader = new ModeLoader(builtInModesDir, userModesDir);
|
|
1737
|
+
await this.modeLoader.loadAll();
|
|
1738
|
+
this.modeDetector = new ModeDetector(this.modeLoader.getAll());
|
|
1739
|
+
this.promptAssembler = new PromptAssembler(this.config.agent, this.modeLoader, this.personalityAdapter ?? undefined);
|
|
1740
|
+
await this.promptAssembler.buildBase();
|
|
1741
|
+
this.userPreferences = this.config.modes?.preferences;
|
|
1742
|
+
this.securityFloor = new SecurityFloor();
|
|
1743
|
+
}
|
|
1744
|
+
getSessionModeState(sessionId) {
|
|
1745
|
+
let state = this.sessionModes.get(sessionId);
|
|
1746
|
+
if (!state) {
|
|
1747
|
+
const defaultMode = this.config.modes?.defaultMode ?? 'auto';
|
|
1748
|
+
state = { activeMode: defaultMode, autoDetected: false };
|
|
1749
|
+
this.sessionModes.set(sessionId, state);
|
|
1750
|
+
}
|
|
1751
|
+
return state;
|
|
1752
|
+
}
|
|
1753
|
+
/** Build enriched prompt with auto-detection (shared by handleMessage and handleChannelMessage). */
|
|
1754
|
+
buildModeEnrichedPrompt(content, modeState, memorySection, channelType) {
|
|
1755
|
+
if (modeState.activeMode === 'auto' && this.modeDetector && this.config.modes?.autoDetection !== false) {
|
|
1756
|
+
const detection = this.modeDetector.detect(content, { currentState: modeState });
|
|
1757
|
+
if (detection) {
|
|
1758
|
+
modeState.lastAutoMode = detection.mode;
|
|
1759
|
+
modeState.autoDetected = true;
|
|
1760
|
+
modeState.lastSwitchAt = Date.now();
|
|
1761
|
+
const tempState = { ...modeState, activeMode: detection.mode };
|
|
1762
|
+
return this.promptAssembler.enrichForMessage(tempState, memorySection, this.userPreferences, undefined, channelType);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return this.promptAssembler.enrichForMessage(modeState, memorySection, this.userPreferences, undefined, channelType);
|
|
1766
|
+
}
|
|
1767
|
+
async handleMessage(client, message) {
|
|
1768
|
+
const { id: requestId, payload } = message;
|
|
1769
|
+
// Handle architect correction messages (learning engine)
|
|
1770
|
+
if (message.type === 'architect_correction') {
|
|
1771
|
+
const corrPayload = payload;
|
|
1772
|
+
if (this.architect && corrPayload?.userMessage && corrPayload.detectedDomain && corrPayload.correctedDomain) {
|
|
1773
|
+
await this.architect.recordCorrection(corrPayload.userMessage, corrPayload.detectedDomain, corrPayload.correctedDomain);
|
|
1774
|
+
}
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
const msgPayload = payload;
|
|
1778
|
+
const content = msgPayload?.content;
|
|
1779
|
+
const modelOverride = msgPayload?.model;
|
|
1780
|
+
const providerOverride = msgPayload?.provider;
|
|
1781
|
+
const thinkingLevel = msgPayload?.thinkingLevel;
|
|
1782
|
+
const chatId = msgPayload?.chatId;
|
|
1783
|
+
if (!content || typeof content !== 'string') {
|
|
1784
|
+
this.sendToClient(client, {
|
|
1785
|
+
type: 'error',
|
|
1786
|
+
id: requestId,
|
|
1787
|
+
payload: { message: 'Missing message content' },
|
|
1788
|
+
});
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
// Handle commands
|
|
1792
|
+
if (content.startsWith('/')) {
|
|
1793
|
+
await this.handleCommand(client, content, requestId);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
// Get or create session — use chatId if provided (multi-chat), otherwise legacy behavior
|
|
1797
|
+
let session;
|
|
1798
|
+
if (chatId) {
|
|
1799
|
+
const existing = await this.sessions.get(chatId);
|
|
1800
|
+
if (existing) {
|
|
1801
|
+
session = existing;
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
session = await this.sessions.create({ channelType: 'webchat', clientId: client.id });
|
|
1805
|
+
this.sendToClient(client, {
|
|
1806
|
+
type: 'chat_created',
|
|
1807
|
+
id: requestId,
|
|
1808
|
+
payload: { chatId: session.id },
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
else {
|
|
1813
|
+
session = await this.sessions.getOrCreate(client.id, {
|
|
1814
|
+
channelType: client.channelType,
|
|
1815
|
+
clientId: client.id,
|
|
1816
|
+
senderId: client.senderId,
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
// Add user message
|
|
1820
|
+
await this.sessions.addMessage(session.id, 'user', content);
|
|
1821
|
+
// Check if providers are available
|
|
1822
|
+
if (!this.providers) {
|
|
1823
|
+
this.sendToClient(client, {
|
|
1824
|
+
type: 'message',
|
|
1825
|
+
id: requestId,
|
|
1826
|
+
payload: {
|
|
1827
|
+
role: 'assistant',
|
|
1828
|
+
content: 'I need API keys to respond. Please add them:\n\n```\nauxiora vault add ANTHROPIC_API_KEY\n```',
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
// Get context messages
|
|
1834
|
+
const contextMessages = this.sessions.getContextMessages(session.id, this.getProviderMaxTokens(this.providers.getPrimaryProvider()), 4096);
|
|
1835
|
+
const chatMessages = sanitizeTranscript(contextMessages).map((m) => ({
|
|
1836
|
+
role: m.role,
|
|
1837
|
+
content: m.content,
|
|
1838
|
+
}));
|
|
1839
|
+
try {
|
|
1840
|
+
// Get tool definitions from registry
|
|
1841
|
+
const tools = toolRegistry.toProviderFormat();
|
|
1842
|
+
// Resolve per-chat personality (metadata overrides global default)
|
|
1843
|
+
const chatRecord = chatId ? this.sessions.getChat(chatId) : undefined;
|
|
1844
|
+
const chatPersonality = chatRecord?.metadata?.personality;
|
|
1845
|
+
const useArchitect = chatPersonality
|
|
1846
|
+
? chatPersonality === 'the-architect'
|
|
1847
|
+
: this.config.agent.personality === 'the-architect';
|
|
1848
|
+
const basePrompt = useArchitect ? this.architectPrompt : this.standardPrompt;
|
|
1849
|
+
// Build enriched prompt with modes and memories
|
|
1850
|
+
let enrichedPrompt = basePrompt;
|
|
1851
|
+
let memorySection = null;
|
|
1852
|
+
if (this.memoryRetriever && this.memoryStore) {
|
|
1853
|
+
const memories = await this.memoryStore.getAll();
|
|
1854
|
+
memorySection = this.memoryRetriever.retrieve(memories, content);
|
|
1855
|
+
}
|
|
1856
|
+
if (this.promptAssembler && this.config.modes?.enabled !== false) {
|
|
1857
|
+
const modeState = this.getSessionModeState(session.id);
|
|
1858
|
+
// Security context check — BEFORE mode detection
|
|
1859
|
+
if (this.securityFloor) {
|
|
1860
|
+
const securityContext = this.securityFloor.detectSecurityContext({ userMessage: content });
|
|
1861
|
+
if (securityContext.active) {
|
|
1862
|
+
// Suspend current mode and use security floor prompt
|
|
1863
|
+
modeState.suspendedMode = modeState.activeMode;
|
|
1864
|
+
enrichedPrompt = this.promptAssembler.enrichForSecurityContext(securityContext, this.securityFloor, memorySection);
|
|
1865
|
+
}
|
|
1866
|
+
else if (modeState.suspendedMode) {
|
|
1867
|
+
// Restore suspended mode
|
|
1868
|
+
modeState.activeMode = modeState.suspendedMode;
|
|
1869
|
+
delete modeState.suspendedMode;
|
|
1870
|
+
enrichedPrompt = this.promptAssembler.enrichForMessage(modeState, memorySection, this.userPreferences, undefined, 'webchat');
|
|
1871
|
+
}
|
|
1872
|
+
else {
|
|
1873
|
+
// Normal mode detection
|
|
1874
|
+
enrichedPrompt = this.buildModeEnrichedPrompt(content, modeState, memorySection, 'webchat');
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
else {
|
|
1878
|
+
// No security floor — normal mode detection
|
|
1879
|
+
enrichedPrompt = this.buildModeEnrichedPrompt(content, modeState, memorySection, 'webchat');
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
else if (memorySection) {
|
|
1883
|
+
enrichedPrompt = basePrompt + memorySection;
|
|
1884
|
+
}
|
|
1885
|
+
// Only apply Architect enrichment if this chat uses the Architect
|
|
1886
|
+
const architectResult = useArchitect
|
|
1887
|
+
? this.applyArchitectEnrichment(enrichedPrompt, content, chatId)
|
|
1888
|
+
: { prompt: enrichedPrompt };
|
|
1889
|
+
enrichedPrompt = architectResult.prompt;
|
|
1890
|
+
// Inject dynamic self-awareness context
|
|
1891
|
+
if (this.selfAwarenessAssembler) {
|
|
1892
|
+
const awarenessContext = {
|
|
1893
|
+
userId: client.senderId ?? 'anonymous',
|
|
1894
|
+
sessionId: session.id,
|
|
1895
|
+
chatId: chatId ?? session.id,
|
|
1896
|
+
currentMessage: content,
|
|
1897
|
+
recentMessages: contextMessages,
|
|
1898
|
+
};
|
|
1899
|
+
const awarenessFragment = await this.selfAwarenessAssembler.assemble(awarenessContext);
|
|
1900
|
+
if (awarenessFragment) {
|
|
1901
|
+
enrichedPrompt += '\n\n[Dynamic Self-Awareness]\n' + awarenessFragment;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
// Route to best model for this message
|
|
1905
|
+
let provider;
|
|
1906
|
+
let routingResult;
|
|
1907
|
+
if (providerOverride || modelOverride) {
|
|
1908
|
+
// Manual override — skip router
|
|
1909
|
+
provider = this.providers.getProvider(providerOverride || this.config.provider.primary);
|
|
1910
|
+
}
|
|
1911
|
+
else if (this.modelRouter && this.config.routing?.enabled !== false) {
|
|
1912
|
+
try {
|
|
1913
|
+
routingResult = this.modelRouter.route(content, { hasImages: false });
|
|
1914
|
+
provider = this.providers.getProvider(routingResult.selection.provider);
|
|
1915
|
+
}
|
|
1916
|
+
catch {
|
|
1917
|
+
provider = this.providers.getPrimaryProvider();
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
else {
|
|
1921
|
+
provider = this.providers.getPrimaryProvider();
|
|
1922
|
+
}
|
|
1923
|
+
// Execute streaming AI call with tool follow-up loop
|
|
1924
|
+
const fallbackCandidates = this.providers.resolveFallbackCandidates();
|
|
1925
|
+
const { response: fullResponse, usage } = await this.executeWithTools(session.id, chatMessages, enrichedPrompt, provider, (type, data) => {
|
|
1926
|
+
if (type === 'text') {
|
|
1927
|
+
this.sendToClient(client, { type: 'chunk', id: requestId, payload: { content: data } });
|
|
1928
|
+
}
|
|
1929
|
+
else if (type === 'thinking') {
|
|
1930
|
+
this.sendToClient(client, { type: 'thinking', id: requestId, payload: { content: data } });
|
|
1931
|
+
}
|
|
1932
|
+
else if (type === 'tool_use') {
|
|
1933
|
+
this.sendToClient(client, { type: 'tool_use', id: requestId, payload: data });
|
|
1934
|
+
}
|
|
1935
|
+
else if (type === 'tool_result') {
|
|
1936
|
+
this.sendToClient(client, { type: 'tool_result', id: requestId, payload: data });
|
|
1937
|
+
}
|
|
1938
|
+
else if (type === 'status') {
|
|
1939
|
+
this.sendToClient(client, { type: 'status', id: requestId, payload: data });
|
|
1940
|
+
}
|
|
1941
|
+
}, { tools, fallbackCandidates });
|
|
1942
|
+
// Save assistant message (skip if empty — happens when response is tool-only)
|
|
1943
|
+
if (fullResponse) {
|
|
1944
|
+
await this.sessions.addMessage(session.id, 'assistant', fullResponse, {
|
|
1945
|
+
input: usage.inputTokens,
|
|
1946
|
+
output: usage.outputTokens,
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
// Record usage for cost tracking
|
|
1950
|
+
if (this.modelRouter && routingResult) {
|
|
1951
|
+
this.modelRouter.recordUsage(routingResult.selection.provider, routingResult.selection.model, usage.inputTokens, usage.outputTokens);
|
|
1952
|
+
}
|
|
1953
|
+
// Extract memories and learn from conversation (if auto-extract enabled)
|
|
1954
|
+
if (this.config.memory?.autoExtract !== false && this.memoryStore && fullResponse && content.length > 20) {
|
|
1955
|
+
void this.extractAndLearn(content, fullResponse, session.id);
|
|
1956
|
+
}
|
|
1957
|
+
// Auto-title webchat chats after first exchange
|
|
1958
|
+
if (fullResponse &&
|
|
1959
|
+
session.metadata.channelType === 'webchat' &&
|
|
1960
|
+
session.messages.length <= 3) {
|
|
1961
|
+
void this.generateChatTitle(session.id, content, fullResponse, client);
|
|
1962
|
+
}
|
|
1963
|
+
// Send done signal
|
|
1964
|
+
this.sendToClient(client, {
|
|
1965
|
+
type: 'done',
|
|
1966
|
+
id: requestId,
|
|
1967
|
+
payload: {
|
|
1968
|
+
usage,
|
|
1969
|
+
routing: routingResult ? {
|
|
1970
|
+
model: routingResult.selection.model,
|
|
1971
|
+
provider: routingResult.selection.provider,
|
|
1972
|
+
isLocal: routingResult.selection.isLocal,
|
|
1973
|
+
taskType: routingResult.classification.type,
|
|
1974
|
+
} : (providerOverride || modelOverride) ? {
|
|
1975
|
+
model: modelOverride,
|
|
1976
|
+
provider: providerOverride || this.config.provider.primary,
|
|
1977
|
+
override: true,
|
|
1978
|
+
} : undefined,
|
|
1979
|
+
architect: architectResult.architectMeta,
|
|
1980
|
+
},
|
|
1981
|
+
});
|
|
1982
|
+
// Background self-awareness analysis
|
|
1983
|
+
if (this.selfAwarenessAssembler) {
|
|
1984
|
+
this.selfAwarenessAssembler.afterResponse({
|
|
1985
|
+
userId: client.senderId ?? 'anonymous',
|
|
1986
|
+
sessionId: session.id,
|
|
1987
|
+
chatId: chatId ?? session.id,
|
|
1988
|
+
currentMessage: content,
|
|
1989
|
+
recentMessages: contextMessages,
|
|
1990
|
+
response: fullResponse,
|
|
1991
|
+
responseTime: Date.now() - (session.metadata.lastActiveAt ?? Date.now()),
|
|
1992
|
+
tokensUsed: { input: usage?.inputTokens ?? 0, output: usage?.outputTokens ?? 0 },
|
|
1993
|
+
}).catch(() => { });
|
|
1994
|
+
}
|
|
1995
|
+
audit('message.sent', {
|
|
1996
|
+
sessionId: session.id,
|
|
1997
|
+
inputTokens: usage.inputTokens,
|
|
1998
|
+
outputTokens: usage.outputTokens,
|
|
1999
|
+
model: routingResult?.selection.model,
|
|
2000
|
+
provider: routingResult?.selection.provider,
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
catch (error) {
|
|
2004
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
2005
|
+
audit('channel.error', { sessionId: session.id, error: errorMessage });
|
|
2006
|
+
this.sendToClient(client, {
|
|
2007
|
+
type: 'error',
|
|
2008
|
+
id: requestId,
|
|
2009
|
+
payload: { message: `Error: ${errorMessage}` },
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
async generateChatTitle(chatId, userMessage, assistantResponse, client) {
|
|
2014
|
+
try {
|
|
2015
|
+
// Check if the chat is still titled "New Chat"
|
|
2016
|
+
const chat = this.sessions.listChats().find(c => c.id === chatId);
|
|
2017
|
+
if (!chat || chat.title !== 'New Chat')
|
|
2018
|
+
return;
|
|
2019
|
+
const provider = this.providers?.getPrimaryProvider();
|
|
2020
|
+
if (!provider)
|
|
2021
|
+
return;
|
|
2022
|
+
const titlePrompt = 'Generate a very short title (3-6 words, no quotes, no punctuation at end) for this conversation. Reply with ONLY the title.';
|
|
2023
|
+
const result = await provider.complete([
|
|
2024
|
+
{ role: 'user', content: userMessage },
|
|
2025
|
+
{ role: 'assistant', content: assistantResponse.slice(0, 500) },
|
|
2026
|
+
{ role: 'user', content: titlePrompt },
|
|
2027
|
+
], { maxTokens: 30 });
|
|
2028
|
+
const title = result.content.trim().replace(/^["']|["']$/g, '').slice(0, 60);
|
|
2029
|
+
if (title) {
|
|
2030
|
+
this.sessions.renameChat(chatId, title);
|
|
2031
|
+
this.sendToClient(client, {
|
|
2032
|
+
type: 'chat_titled',
|
|
2033
|
+
payload: { chatId, title },
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
catch {
|
|
2038
|
+
// Non-fatal — chat keeps "New Chat" title
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
async handleCommand(client, command, requestId) {
|
|
2042
|
+
const [cmd, ...args] = command.slice(1).split(' ');
|
|
2043
|
+
switch (cmd.toLowerCase()) {
|
|
2044
|
+
case 'status': {
|
|
2045
|
+
const activeSessions = this.sessions.getActiveSessions();
|
|
2046
|
+
const providers = this.providers?.listAvailable() || [];
|
|
2047
|
+
this.sendToClient(client, {
|
|
2048
|
+
type: 'message',
|
|
2049
|
+
id: requestId,
|
|
2050
|
+
payload: {
|
|
2051
|
+
role: 'assistant',
|
|
2052
|
+
content: `**Status**\n- Sessions: ${activeSessions.length} active\n- Providers: ${providers.join(', ') || 'none configured'}\n- Uptime: ${Math.floor(process.uptime())}s`,
|
|
2053
|
+
},
|
|
2054
|
+
});
|
|
2055
|
+
break;
|
|
2056
|
+
}
|
|
2057
|
+
case 'new':
|
|
2058
|
+
case 'reset': {
|
|
2059
|
+
const session = await this.sessions.getOrCreate(client.id, {
|
|
2060
|
+
channelType: client.channelType,
|
|
2061
|
+
clientId: client.id,
|
|
2062
|
+
});
|
|
2063
|
+
await this.sessions.clear(session.id);
|
|
2064
|
+
this.sendToClient(client, {
|
|
2065
|
+
type: 'message',
|
|
2066
|
+
id: requestId,
|
|
2067
|
+
payload: {
|
|
2068
|
+
role: 'assistant',
|
|
2069
|
+
content: 'Session cleared. Starting fresh!',
|
|
2070
|
+
},
|
|
2071
|
+
});
|
|
2072
|
+
break;
|
|
2073
|
+
}
|
|
2074
|
+
case 'mode': {
|
|
2075
|
+
const session = await this.sessions.getOrCreate(client.id, {
|
|
2076
|
+
channelType: client.channelType,
|
|
2077
|
+
clientId: client.id,
|
|
2078
|
+
});
|
|
2079
|
+
if (this.config.modes?.enabled === false || !this.modeLoader) {
|
|
2080
|
+
this.sendToClient(client, {
|
|
2081
|
+
type: 'message',
|
|
2082
|
+
id: requestId,
|
|
2083
|
+
payload: { role: 'assistant', content: 'Modes are disabled in configuration.' },
|
|
2084
|
+
});
|
|
2085
|
+
break;
|
|
2086
|
+
}
|
|
2087
|
+
const subCmd = args[0]?.toLowerCase();
|
|
2088
|
+
const modeState = this.getSessionModeState(session.id);
|
|
2089
|
+
if (!subCmd || subCmd === 'status') {
|
|
2090
|
+
const modes = this.modeLoader.getAll();
|
|
2091
|
+
const modeList = [...modes.values()].map(m => `- **${m.name}** (\`${m.id}\`) — ${m.description}`).join('\n');
|
|
2092
|
+
const currentLabel = modeState.activeMode === 'auto' ? 'auto (auto-detect)' : modeState.activeMode === 'off' ? 'off' : modeState.activeMode;
|
|
2093
|
+
this.sendToClient(client, {
|
|
2094
|
+
type: 'message',
|
|
2095
|
+
id: requestId,
|
|
2096
|
+
payload: {
|
|
2097
|
+
role: 'assistant',
|
|
2098
|
+
content: `**Current mode:** ${currentLabel}${modeState.lastAutoMode ? ` (last detected: ${modeState.lastAutoMode})` : ''}\n\n**Available modes:**\n${modeList}\n\n**Commands:** \`/mode <name>\`, \`/mode auto\`, \`/mode off\``,
|
|
2099
|
+
},
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
else if (subCmd === 'auto') {
|
|
2103
|
+
modeState.activeMode = 'auto';
|
|
2104
|
+
modeState.autoDetected = false;
|
|
2105
|
+
modeState.lastSwitchAt = Date.now();
|
|
2106
|
+
this.sendToClient(client, {
|
|
2107
|
+
type: 'message',
|
|
2108
|
+
id: requestId,
|
|
2109
|
+
payload: { role: 'assistant', content: 'Mode set to **auto**. I will detect the best mode from your messages.' },
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
else if (subCmd === 'off') {
|
|
2113
|
+
modeState.activeMode = 'off';
|
|
2114
|
+
modeState.autoDetected = false;
|
|
2115
|
+
modeState.lastSwitchAt = Date.now();
|
|
2116
|
+
this.sendToClient(client, {
|
|
2117
|
+
type: 'message',
|
|
2118
|
+
id: requestId,
|
|
2119
|
+
payload: { role: 'assistant', content: 'Modes disabled for this session.' },
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
else if (MODE_IDS.includes(subCmd)) {
|
|
2123
|
+
modeState.activeMode = subCmd;
|
|
2124
|
+
modeState.autoDetected = false;
|
|
2125
|
+
modeState.lastSwitchAt = Date.now();
|
|
2126
|
+
const mode = this.modeLoader.get(subCmd);
|
|
2127
|
+
this.sendToClient(client, {
|
|
2128
|
+
type: 'message',
|
|
2129
|
+
id: requestId,
|
|
2130
|
+
payload: { role: 'assistant', content: `Switched to **${mode?.name ?? subCmd}** mode.` },
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
else {
|
|
2134
|
+
this.sendToClient(client, {
|
|
2135
|
+
type: 'message',
|
|
2136
|
+
id: requestId,
|
|
2137
|
+
payload: { role: 'assistant', content: `Unknown mode: ${subCmd}. Use \`/mode\` to see available modes.` },
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
2142
|
+
case 'help': {
|
|
2143
|
+
this.sendToClient(client, {
|
|
2144
|
+
type: 'message',
|
|
2145
|
+
id: requestId,
|
|
2146
|
+
payload: {
|
|
2147
|
+
role: 'assistant',
|
|
2148
|
+
content: `**Commands**\n- /status - Show system status\n- /new - Start a new session\n- /reset - Clear current session\n- /mode - Show/switch personality modes\n- /help - Show this help`,
|
|
2149
|
+
},
|
|
2150
|
+
});
|
|
2151
|
+
break;
|
|
2152
|
+
}
|
|
2153
|
+
default: {
|
|
2154
|
+
this.sendToClient(client, {
|
|
2155
|
+
type: 'message',
|
|
2156
|
+
id: requestId,
|
|
2157
|
+
payload: {
|
|
2158
|
+
role: 'assistant',
|
|
2159
|
+
content: `Unknown command: ${cmd}. Try /help`,
|
|
2160
|
+
},
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Execute a streaming AI call with tool follow-up loop.
|
|
2167
|
+
* When the AI calls tools, executes them and feeds results back to the AI
|
|
2168
|
+
* for synthesis, looping up to maxToolRounds times.
|
|
2169
|
+
*/
|
|
2170
|
+
async executeWithTools(sessionId, messages, enrichedPrompt, provider, onChunk, options) {
|
|
2171
|
+
const maxRounds = options?.maxToolRounds ?? 10;
|
|
2172
|
+
const maxContinuations = 3; // Safety cap for auto-continue on truncation
|
|
2173
|
+
const tools = options?.tools ?? toolRegistry.toProviderFormat();
|
|
2174
|
+
let currentMessages = [...messages];
|
|
2175
|
+
let totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
2176
|
+
let fullResponse = '';
|
|
2177
|
+
let lastRoundHadTools = false;
|
|
2178
|
+
const loopState = createLoopDetectionState();
|
|
2179
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
2180
|
+
let roundResponse = '';
|
|
2181
|
+
let roundUsage = { inputTokens: 0, outputTokens: 0 };
|
|
2182
|
+
let roundFinishReason = '';
|
|
2183
|
+
const toolUses = [];
|
|
2184
|
+
const streamOptions = {
|
|
2185
|
+
systemPrompt: enrichedPrompt,
|
|
2186
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
2187
|
+
passThroughAllTools: true,
|
|
2188
|
+
};
|
|
2189
|
+
const candidates = options?.fallbackCandidates ?? [
|
|
2190
|
+
{ provider, name: provider.name, model: provider.defaultModel },
|
|
2191
|
+
];
|
|
2192
|
+
for await (const chunk of streamWithModelFallback({ candidates }, (p) => p.stream(currentMessages, streamOptions))) {
|
|
2193
|
+
if (chunk.type === 'text' && chunk.content) {
|
|
2194
|
+
roundResponse += chunk.content;
|
|
2195
|
+
onChunk('text', chunk.content);
|
|
2196
|
+
}
|
|
2197
|
+
else if (chunk.type === 'thinking' && chunk.content) {
|
|
2198
|
+
onChunk('thinking', chunk.content);
|
|
2199
|
+
}
|
|
2200
|
+
else if (chunk.type === 'tool_use' && chunk.toolUse) {
|
|
2201
|
+
toolUses.push(chunk.toolUse);
|
|
2202
|
+
onChunk('tool_use', { tool: chunk.toolUse.name, params: chunk.toolUse.input });
|
|
2203
|
+
}
|
|
2204
|
+
else if (chunk.type === 'done') {
|
|
2205
|
+
roundUsage = chunk.usage || roundUsage;
|
|
2206
|
+
roundFinishReason = chunk.finishReason || '';
|
|
2207
|
+
}
|
|
2208
|
+
else if (chunk.type === 'error') {
|
|
2209
|
+
throw new Error(chunk.error);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
totalUsage.inputTokens += roundUsage.inputTokens;
|
|
2213
|
+
totalUsage.outputTokens += roundUsage.outputTokens;
|
|
2214
|
+
// No tool calls — check if response was truncated
|
|
2215
|
+
if (toolUses.length === 0) {
|
|
2216
|
+
fullResponse += roundResponse;
|
|
2217
|
+
// Auto-continue if response was cut off by token limit
|
|
2218
|
+
const wasTruncated = roundFinishReason === 'max_tokens' || roundFinishReason === 'length';
|
|
2219
|
+
if (wasTruncated && fullResponse.length > 0) {
|
|
2220
|
+
let continuations = 0;
|
|
2221
|
+
while (continuations < maxContinuations) {
|
|
2222
|
+
continuations++;
|
|
2223
|
+
this.logger.info('Response truncated, auto-continuing', { continuations, finishReason: roundFinishReason });
|
|
2224
|
+
currentMessages.push({ role: 'assistant', content: fullResponse });
|
|
2225
|
+
currentMessages.push({ role: 'user', content: 'Continue where you left off.' });
|
|
2226
|
+
let contResponse = '';
|
|
2227
|
+
let contUsage = { inputTokens: 0, outputTokens: 0 };
|
|
2228
|
+
let contFinishReason = '';
|
|
2229
|
+
for await (const chunk of provider.stream(currentMessages, {
|
|
2230
|
+
systemPrompt: enrichedPrompt,
|
|
2231
|
+
})) {
|
|
2232
|
+
if (chunk.type === 'text' && chunk.content) {
|
|
2233
|
+
contResponse += chunk.content;
|
|
2234
|
+
onChunk('text', chunk.content);
|
|
2235
|
+
}
|
|
2236
|
+
else if (chunk.type === 'done') {
|
|
2237
|
+
contUsage = chunk.usage || contUsage;
|
|
2238
|
+
contFinishReason = chunk.finishReason || '';
|
|
2239
|
+
}
|
|
2240
|
+
else if (chunk.type === 'error') {
|
|
2241
|
+
throw new Error(chunk.error);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
totalUsage.inputTokens += contUsage.inputTokens;
|
|
2245
|
+
totalUsage.outputTokens += contUsage.outputTokens;
|
|
2246
|
+
fullResponse += contResponse;
|
|
2247
|
+
// Stop if the model finished naturally
|
|
2248
|
+
if (contFinishReason !== 'max_tokens' && contFinishReason !== 'length') {
|
|
2249
|
+
break;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
lastRoundHadTools = false;
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2256
|
+
lastRoundHadTools = true;
|
|
2257
|
+
// Record the assistant's response (including tool use intent) in the conversation
|
|
2258
|
+
const assistantContent = roundResponse || `I'll use ${toolUses.map(t => t.name).join(', ')} to help with this.`;
|
|
2259
|
+
currentMessages.push({ role: 'assistant', content: assistantContent });
|
|
2260
|
+
await this.sessions.addMessage(sessionId, 'assistant', assistantContent);
|
|
2261
|
+
// Execute tools and collect results
|
|
2262
|
+
const context = {
|
|
2263
|
+
sessionId,
|
|
2264
|
+
workingDirectory: getWorkspacePath(),
|
|
2265
|
+
timeout: 30000,
|
|
2266
|
+
};
|
|
2267
|
+
const toolResultParts = [];
|
|
2268
|
+
for (const toolUse of toolUses) {
|
|
2269
|
+
// Map Claude Code emulation tool names to our actual tools
|
|
2270
|
+
const mapped = mapCCToolCall(toolUse.name, toolUse.input);
|
|
2271
|
+
recordToolCall(loopState, toolUse.id, mapped.name, mapped.input);
|
|
2272
|
+
try {
|
|
2273
|
+
const result = await toolExecutor.execute(mapped.name, mapped.input, context);
|
|
2274
|
+
onChunk('tool_result', {
|
|
2275
|
+
tool: toolUse.name,
|
|
2276
|
+
success: result.success,
|
|
2277
|
+
output: result.output,
|
|
2278
|
+
error: result.error,
|
|
2279
|
+
});
|
|
2280
|
+
// Truncate large tool outputs to avoid blowing context window
|
|
2281
|
+
let output = result.success ? (result.output || 'Success') : `Error: ${result.error}`;
|
|
2282
|
+
if (output.length > 50000) {
|
|
2283
|
+
output = output.slice(0, 50000) + '\n... [truncated]';
|
|
2284
|
+
}
|
|
2285
|
+
toolResultParts.push(`[${toolUse.name}]: ${output}`);
|
|
2286
|
+
recordToolOutcome(loopState, toolUse.id, output);
|
|
2287
|
+
}
|
|
2288
|
+
catch (error) {
|
|
2289
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
2290
|
+
onChunk('tool_result', { tool: toolUse.name, success: false, error: errorMessage });
|
|
2291
|
+
toolResultParts.push(`[${toolUse.name}]: Error: ${errorMessage}`);
|
|
2292
|
+
recordToolOutcome(loopState, toolUse.id, errorMessage);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
// Append tool results directly to conversation (don't rebuild from getContextMessages
|
|
2296
|
+
// which can drop messages due to token windowing)
|
|
2297
|
+
const toolResultsMessage = `[Tool Results]\n${toolResultParts.join('\n')}`;
|
|
2298
|
+
currentMessages.push({ role: 'user', content: toolResultsMessage });
|
|
2299
|
+
await this.sessions.addMessage(sessionId, 'user', toolResultsMessage);
|
|
2300
|
+
// Check for tool loop patterns
|
|
2301
|
+
const detection = detectLoop(loopState);
|
|
2302
|
+
if (detection.severity === 'critical') {
|
|
2303
|
+
this.logger.warn('Tool loop detected — forcing synthesis', {
|
|
2304
|
+
detector: detection.detector,
|
|
2305
|
+
message: detection.message,
|
|
2306
|
+
details: detection.details,
|
|
2307
|
+
});
|
|
2308
|
+
onChunk('status', { message: 'Loop detected, synthesizing results...' });
|
|
2309
|
+
lastRoundHadTools = true;
|
|
2310
|
+
break;
|
|
2311
|
+
}
|
|
2312
|
+
if (detection.severity === 'warning') {
|
|
2313
|
+
this.logger.info('Tool loop warning', {
|
|
2314
|
+
detector: detection.detector,
|
|
2315
|
+
message: detection.message,
|
|
2316
|
+
});
|
|
2317
|
+
currentMessages.push({ role: 'user', content: `⚠️ Loop detection warning: ${detection.message}\nPlease try a different approach or different parameters.` });
|
|
2318
|
+
}
|
|
2319
|
+
// Notify the client that tool processing is done and AI is thinking about results
|
|
2320
|
+
onChunk('status', { message: 'Analyzing results...' });
|
|
2321
|
+
}
|
|
2322
|
+
// If the loop ended because we hit maxRounds while still using tools,
|
|
2323
|
+
// do one final call WITHOUT tools to force a synthesis of all gathered info
|
|
2324
|
+
if (lastRoundHadTools) {
|
|
2325
|
+
let synthesisResponse = '';
|
|
2326
|
+
let synthesisUsage = { inputTokens: 0, outputTokens: 0 };
|
|
2327
|
+
currentMessages.push({ role: 'user', content: 'Now synthesize all the information gathered above into your final response. Do not call any more tools.' });
|
|
2328
|
+
for await (const chunk of provider.stream(currentMessages, {
|
|
2329
|
+
systemPrompt: enrichedPrompt,
|
|
2330
|
+
// No tools — force text-only synthesis
|
|
2331
|
+
})) {
|
|
2332
|
+
if (chunk.type === 'text' && chunk.content) {
|
|
2333
|
+
synthesisResponse += chunk.content;
|
|
2334
|
+
onChunk('text', chunk.content);
|
|
2335
|
+
}
|
|
2336
|
+
else if (chunk.type === 'done') {
|
|
2337
|
+
synthesisUsage = chunk.usage || synthesisUsage;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
totalUsage.inputTokens += synthesisUsage.inputTokens;
|
|
2341
|
+
totalUsage.outputTokens += synthesisUsage.outputTokens;
|
|
2342
|
+
fullResponse = synthesisResponse; // Replace accumulated "thinking out loud" with actual synthesis
|
|
2343
|
+
}
|
|
2344
|
+
return { response: fullResponse, usage: totalUsage };
|
|
2345
|
+
}
|
|
2346
|
+
async handleVoiceMessage(client, type, payload, audioBuffer) {
|
|
2347
|
+
if (!this.voiceManager) {
|
|
2348
|
+
this.sendToClient(client, {
|
|
2349
|
+
type: 'voice_error',
|
|
2350
|
+
payload: { message: 'Voice mode not available' },
|
|
2351
|
+
});
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
try {
|
|
2355
|
+
if (type === 'voice_start') {
|
|
2356
|
+
const opts = payload;
|
|
2357
|
+
this.voiceManager.startSession(client.id, {
|
|
2358
|
+
voice: opts?.voice,
|
|
2359
|
+
language: opts?.language,
|
|
2360
|
+
});
|
|
2361
|
+
this.sendToClient(client, { type: 'voice_ready' });
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
if (type === 'voice_cancel') {
|
|
2365
|
+
this.voiceManager.endSession(client.id);
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (type === 'voice_end' && audioBuffer) {
|
|
2369
|
+
// Feed audio into voice manager buffer then transcribe
|
|
2370
|
+
this.voiceManager.addAudioFrame(client.id, audioBuffer);
|
|
2371
|
+
const transcription = await this.voiceManager.transcribe(client.id);
|
|
2372
|
+
this.sendToClient(client, {
|
|
2373
|
+
type: 'voice_transcript',
|
|
2374
|
+
payload: { text: transcription.text, final: true },
|
|
2375
|
+
});
|
|
2376
|
+
audit('voice.transcribed', {
|
|
2377
|
+
clientId: client.id,
|
|
2378
|
+
duration: transcription.duration,
|
|
2379
|
+
language: transcription.language,
|
|
2380
|
+
textLength: transcription.text.length,
|
|
2381
|
+
});
|
|
2382
|
+
// Feed transcribed text into AI pipeline
|
|
2383
|
+
if (!this.providers) {
|
|
2384
|
+
this.sendToClient(client, {
|
|
2385
|
+
type: 'voice_error',
|
|
2386
|
+
payload: { message: 'AI providers not configured' },
|
|
2387
|
+
});
|
|
2388
|
+
this.voiceManager.endSession(client.id);
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
const session = await this.sessions.getOrCreate(client.id, {
|
|
2392
|
+
channelType: client.channelType,
|
|
2393
|
+
clientId: client.id,
|
|
2394
|
+
senderId: client.senderId,
|
|
2395
|
+
});
|
|
2396
|
+
await this.sessions.addMessage(session.id, 'user', transcription.text);
|
|
2397
|
+
const contextMessages = this.sessions.getContextMessages(session.id, this.getProviderMaxTokens(this.providers.getPrimaryProvider()), 4096);
|
|
2398
|
+
const chatMessages = sanitizeTranscript(contextMessages).map((m) => ({
|
|
2399
|
+
role: m.role,
|
|
2400
|
+
content: m.content,
|
|
2401
|
+
}));
|
|
2402
|
+
// Enrich prompt with memories (same as text flow)
|
|
2403
|
+
let voicePrompt = this.systemPrompt;
|
|
2404
|
+
if (this.memoryRetriever && this.memoryStore) {
|
|
2405
|
+
const memories = await this.memoryStore.getAll();
|
|
2406
|
+
const memorySection = this.memoryRetriever.retrieve(memories, transcription.text);
|
|
2407
|
+
if (memorySection) {
|
|
2408
|
+
voicePrompt = this.systemPrompt + memorySection;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
// Use executeWithTools for voice — tools execute silently, only final text goes to TTS
|
|
2412
|
+
const provider = this.providers.getPrimaryProvider();
|
|
2413
|
+
const fallbackCandidates = this.providers.resolveFallbackCandidates();
|
|
2414
|
+
const { response: voiceResponse, usage: voiceUsage } = await this.executeWithTools(session.id, chatMessages, voicePrompt, provider, (_type, _data) => {
|
|
2415
|
+
// Voice: don't stream chunks to client — we synthesize the final text
|
|
2416
|
+
}, { fallbackCandidates });
|
|
2417
|
+
await this.sessions.addMessage(session.id, 'assistant', voiceResponse, {
|
|
2418
|
+
input: voiceUsage.inputTokens,
|
|
2419
|
+
output: voiceUsage.outputTokens,
|
|
2420
|
+
});
|
|
2421
|
+
// Extract memories from voice conversation
|
|
2422
|
+
if (this.config.memory?.autoExtract !== false && this.memoryStore && voiceResponse && transcription.text.length > 20) {
|
|
2423
|
+
void this.extractAndLearn(transcription.text, voiceResponse, session.id);
|
|
2424
|
+
}
|
|
2425
|
+
// Send text response
|
|
2426
|
+
this.sendToClient(client, {
|
|
2427
|
+
type: 'voice_text',
|
|
2428
|
+
payload: { content: voiceResponse },
|
|
2429
|
+
});
|
|
2430
|
+
// Stream TTS audio
|
|
2431
|
+
for await (const chunk of this.voiceManager.synthesize(client.id, voiceResponse)) {
|
|
2432
|
+
this.gateway.sendBinary(client, chunk);
|
|
2433
|
+
}
|
|
2434
|
+
audit('voice.synthesized', {
|
|
2435
|
+
clientId: client.id,
|
|
2436
|
+
textLength: voiceResponse.length,
|
|
2437
|
+
voice: this.config.voice?.defaultVoice ?? 'alloy',
|
|
2438
|
+
});
|
|
2439
|
+
this.sendToClient(client, { type: 'voice_end' });
|
|
2440
|
+
this.voiceManager.endSession(client.id);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
catch (error) {
|
|
2444
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
2445
|
+
this.sendToClient(client, {
|
|
2446
|
+
type: 'voice_error',
|
|
2447
|
+
payload: { message: errorMessage },
|
|
2448
|
+
});
|
|
2449
|
+
this.voiceManager.endSession(client.id);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
sendToClient(client, message) {
|
|
2453
|
+
if (client.ws.readyState === 1) {
|
|
2454
|
+
// WebSocket.OPEN
|
|
2455
|
+
client.ws.send(JSON.stringify(message));
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
/** Load persisted channel targets from disk so behavior delivery survives restarts. */
|
|
2459
|
+
async loadChannelTargets() {
|
|
2460
|
+
try {
|
|
2461
|
+
const data = await fs.readFile(this.channelTargetsPath, 'utf-8');
|
|
2462
|
+
const targets = JSON.parse(data);
|
|
2463
|
+
for (const [channelType, channelId] of Object.entries(targets)) {
|
|
2464
|
+
// Only load if we don't already have a fresher entry from this session
|
|
2465
|
+
if (!this.lastActiveChannels.has(channelType)) {
|
|
2466
|
+
this.lastActiveChannels.set(channelType, channelId);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
this.logger.debug('Loaded channel targets', { channels: Object.keys(targets) });
|
|
2470
|
+
}
|
|
2471
|
+
catch {
|
|
2472
|
+
// File doesn't exist yet — that's fine
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
/** Persist channel targets to disk. */
|
|
2476
|
+
async saveChannelTargets() {
|
|
2477
|
+
try {
|
|
2478
|
+
const targets = {};
|
|
2479
|
+
for (const [channelType, channelId] of this.lastActiveChannels) {
|
|
2480
|
+
targets[channelType] = channelId;
|
|
2481
|
+
}
|
|
2482
|
+
await fs.mkdir(path.dirname(this.channelTargetsPath), { recursive: true });
|
|
2483
|
+
await fs.writeFile(this.channelTargetsPath, JSON.stringify(targets, null, 2), 'utf-8');
|
|
2484
|
+
}
|
|
2485
|
+
catch (err) {
|
|
2486
|
+
this.logger.warn('Failed to save channel targets', {
|
|
2487
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
/** Track an agent starting execution. Broadcasts to dashboard. */
|
|
2492
|
+
agentStart(id, type, description, channelType) {
|
|
2493
|
+
const activity = { id, type, description, channelType, startedAt: new Date().toISOString() };
|
|
2494
|
+
this.activeAgents.set(id, activity);
|
|
2495
|
+
this.gateway.broadcast({ type: 'agent_start', payload: activity }, (client) => client.authenticated);
|
|
2496
|
+
}
|
|
2497
|
+
/** Track an agent finishing execution. Broadcasts to dashboard. */
|
|
2498
|
+
agentEnd(id, success) {
|
|
2499
|
+
const activity = this.activeAgents.get(id);
|
|
2500
|
+
if (!activity)
|
|
2501
|
+
return;
|
|
2502
|
+
const duration = Date.now() - new Date(activity.startedAt).getTime();
|
|
2503
|
+
this.activeAgents.delete(id);
|
|
2504
|
+
this.gateway.broadcast({ type: 'agent_end', payload: { id, duration, success } }, (client) => client.authenticated);
|
|
2505
|
+
}
|
|
2506
|
+
/** Get all currently active agents. */
|
|
2507
|
+
getActiveAgents() {
|
|
2508
|
+
return Array.from(this.activeAgents.values());
|
|
2509
|
+
}
|
|
2510
|
+
/** Persist a message to the webchat session so it appears in chat history. */
|
|
2511
|
+
persistToWebchat(content) {
|
|
2512
|
+
this.sessions.getOrCreate('webchat', { channelType: 'webchat' })
|
|
2513
|
+
.then(session => this.sessions.addMessage(session.id, 'assistant', content))
|
|
2514
|
+
.catch(() => { });
|
|
2515
|
+
}
|
|
2516
|
+
/** Deliver a proactive message to all connected channel adapters using tracked channel IDs.
|
|
2517
|
+
* Also persists to the webchat session so messages appear in chat history.
|
|
2518
|
+
* @param exclude - channel type to skip (already delivered by targeted send) */
|
|
2519
|
+
deliverToAllChannels(content, exclude) {
|
|
2520
|
+
this.persistToWebchat(content);
|
|
2521
|
+
// Deliver to external channel adapters (Discord, Slack, Telegram, etc.)
|
|
2522
|
+
if (!this.channels)
|
|
2523
|
+
return;
|
|
2524
|
+
for (const ct of this.channels.getConnectedChannels()) {
|
|
2525
|
+
if (ct === exclude)
|
|
2526
|
+
continue;
|
|
2527
|
+
const targetId = this.lastActiveChannels.get(ct)
|
|
2528
|
+
?? this.channels.getDefaultChannelId(ct);
|
|
2529
|
+
if (!targetId)
|
|
2530
|
+
continue;
|
|
2531
|
+
this.channels.send(ct, targetId, { content }).catch((err) => {
|
|
2532
|
+
this.logger.warn('Proactive channel delivery failed', {
|
|
2533
|
+
channelType: ct,
|
|
2534
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
2535
|
+
});
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
async handleChannelMessage(inbound) {
|
|
2540
|
+
const requestId = generateRequestId();
|
|
2541
|
+
return runWithRequestId(requestId, async () => {
|
|
2542
|
+
// Track last-active channel ID for proactive delivery and persist to disk
|
|
2543
|
+
this.lastActiveChannels.set(inbound.channelType, inbound.channelId);
|
|
2544
|
+
void this.saveChannelTargets();
|
|
2545
|
+
// Get or create session for this sender
|
|
2546
|
+
const session = await this.sessions.getOrCreate(`${inbound.channelType}:${inbound.senderId}`, {
|
|
2547
|
+
channelType: inbound.channelType,
|
|
2548
|
+
senderId: inbound.senderId,
|
|
2549
|
+
});
|
|
2550
|
+
// Handle commands
|
|
2551
|
+
if (inbound.content.startsWith('/')) {
|
|
2552
|
+
const response = await this.handleChannelCommand(inbound.content, session.id);
|
|
2553
|
+
if (this.channels) {
|
|
2554
|
+
await this.channels.send(inbound.channelType, inbound.channelId, {
|
|
2555
|
+
content: response,
|
|
2556
|
+
replyToId: inbound.id,
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
// Process media attachments and add user message
|
|
2562
|
+
let messageContent = inbound.content;
|
|
2563
|
+
if (inbound.attachments && inbound.attachments.length > 0 && this.mediaProcessor) {
|
|
2564
|
+
messageContent = await this.mediaProcessor.process(inbound.attachments, inbound.content);
|
|
2565
|
+
}
|
|
2566
|
+
await this.sessions.addMessage(session.id, 'user', messageContent);
|
|
2567
|
+
// Check if providers are available
|
|
2568
|
+
if (!this.providers) {
|
|
2569
|
+
if (this.channels) {
|
|
2570
|
+
await this.channels.send(inbound.channelType, inbound.channelId, {
|
|
2571
|
+
content: 'I need API keys to respond. Please configure them in the vault.',
|
|
2572
|
+
replyToId: inbound.id,
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
// Get context messages
|
|
2578
|
+
const contextMessages = this.sessions.getContextMessages(session.id, this.getProviderMaxTokens(this.providers.getPrimaryProvider()), 4096);
|
|
2579
|
+
const chatMessages = sanitizeTranscript(contextMessages).map((m) => ({
|
|
2580
|
+
role: m.role,
|
|
2581
|
+
content: m.content,
|
|
2582
|
+
}));
|
|
2583
|
+
// Show typing indicator while generating response
|
|
2584
|
+
const stopTyping = this.channels
|
|
2585
|
+
? await this.channels.startTyping(inbound.channelType, inbound.channelId)
|
|
2586
|
+
: () => { };
|
|
2587
|
+
const channelAgentId = `channel:${inbound.channelType}:${inbound.channelId}:${Date.now()}`;
|
|
2588
|
+
try {
|
|
2589
|
+
// Get tool definitions from registry
|
|
2590
|
+
const tools = toolRegistry.toProviderFormat();
|
|
2591
|
+
// Build enriched prompt with modes and memories
|
|
2592
|
+
let enrichedPrompt = this.systemPrompt;
|
|
2593
|
+
let channelMemorySection = null;
|
|
2594
|
+
if (this.memoryRetriever && this.memoryStore) {
|
|
2595
|
+
const memories = await this.memoryStore.getAll();
|
|
2596
|
+
channelMemorySection = this.memoryRetriever.retrieve(memories, inbound.content);
|
|
2597
|
+
}
|
|
2598
|
+
if (this.promptAssembler && this.config.modes?.enabled !== false) {
|
|
2599
|
+
const modeState = this.getSessionModeState(session.id);
|
|
2600
|
+
// Security context check — BEFORE mode detection
|
|
2601
|
+
if (this.securityFloor) {
|
|
2602
|
+
const securityContext = this.securityFloor.detectSecurityContext({ userMessage: inbound.content });
|
|
2603
|
+
if (securityContext.active) {
|
|
2604
|
+
modeState.suspendedMode = modeState.activeMode;
|
|
2605
|
+
enrichedPrompt = this.promptAssembler.enrichForSecurityContext(securityContext, this.securityFloor, channelMemorySection);
|
|
2606
|
+
}
|
|
2607
|
+
else if (modeState.suspendedMode) {
|
|
2608
|
+
modeState.activeMode = modeState.suspendedMode;
|
|
2609
|
+
delete modeState.suspendedMode;
|
|
2610
|
+
enrichedPrompt = this.buildModeEnrichedPrompt(inbound.content, modeState, channelMemorySection, inbound.channelType);
|
|
2611
|
+
}
|
|
2612
|
+
else {
|
|
2613
|
+
enrichedPrompt = this.buildModeEnrichedPrompt(inbound.content, modeState, channelMemorySection, inbound.channelType);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
else {
|
|
2617
|
+
enrichedPrompt = this.buildModeEnrichedPrompt(inbound.content, modeState, channelMemorySection, inbound.channelType);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
else if (channelMemorySection) {
|
|
2621
|
+
enrichedPrompt = this.systemPrompt + channelMemorySection;
|
|
2622
|
+
}
|
|
2623
|
+
const channelArchitectResult = this.applyArchitectEnrichment(enrichedPrompt, inbound.content);
|
|
2624
|
+
enrichedPrompt = channelArchitectResult.prompt;
|
|
2625
|
+
// Use executeWithTools for channels — collect final text for channel reply
|
|
2626
|
+
const provider = this.providers.getPrimaryProvider();
|
|
2627
|
+
this.agentStart(channelAgentId, 'channel', `Processing message on ${inbound.channelType}`, inbound.channelType);
|
|
2628
|
+
// Draft streaming: edit message in place if adapter supports it
|
|
2629
|
+
const adapter = this.channels?.getAdapter(inbound.channelType);
|
|
2630
|
+
const supportsDraft = !!adapter?.editMessage;
|
|
2631
|
+
let draftMessageId = null;
|
|
2632
|
+
let accumulatedText = '';
|
|
2633
|
+
let draftLoop = null;
|
|
2634
|
+
if (supportsDraft && this.channels) {
|
|
2635
|
+
const channels = this.channels;
|
|
2636
|
+
draftLoop = new DraftStreamLoop(async (text) => {
|
|
2637
|
+
try {
|
|
2638
|
+
if (!draftMessageId) {
|
|
2639
|
+
const result = await channels.send(inbound.channelType, inbound.channelId, {
|
|
2640
|
+
content: text,
|
|
2641
|
+
replyToId: inbound.id,
|
|
2642
|
+
});
|
|
2643
|
+
if (result.success && result.messageId) {
|
|
2644
|
+
draftMessageId = result.messageId;
|
|
2645
|
+
}
|
|
2646
|
+
return result.success;
|
|
2647
|
+
}
|
|
2648
|
+
else {
|
|
2649
|
+
const result = await channels.editMessage(inbound.channelType, inbound.channelId, draftMessageId, { content: text });
|
|
2650
|
+
return result.success;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
catch {
|
|
2654
|
+
return false;
|
|
2655
|
+
}
|
|
2656
|
+
}, 1000);
|
|
2657
|
+
}
|
|
2658
|
+
const fallbackCandidates = this.providers.resolveFallbackCandidates();
|
|
2659
|
+
const { response: channelResponse, usage: channelUsage } = await this.executeWithTools(session.id, chatMessages, enrichedPrompt, provider, (type, data) => {
|
|
2660
|
+
if (type === 'text' && data && draftLoop) {
|
|
2661
|
+
accumulatedText += data;
|
|
2662
|
+
draftLoop.update(accumulatedText);
|
|
2663
|
+
}
|
|
2664
|
+
}, { tools, fallbackCandidates });
|
|
2665
|
+
// Flush final draft text
|
|
2666
|
+
if (draftLoop) {
|
|
2667
|
+
if (channelResponse && channelResponse !== accumulatedText) {
|
|
2668
|
+
draftLoop.update(channelResponse);
|
|
2669
|
+
}
|
|
2670
|
+
await draftLoop.flush();
|
|
2671
|
+
draftLoop.stop();
|
|
2672
|
+
}
|
|
2673
|
+
stopTyping();
|
|
2674
|
+
// Save assistant message
|
|
2675
|
+
await this.sessions.addMessage(session.id, 'assistant', channelResponse, {
|
|
2676
|
+
input: channelUsage.inputTokens,
|
|
2677
|
+
output: channelUsage.outputTokens,
|
|
2678
|
+
});
|
|
2679
|
+
// Extract memories and learn from conversation (if auto-extract enabled)
|
|
2680
|
+
if (this.config.memory?.autoExtract !== false && this.memoryStore && channelResponse && inbound.content.length > 20) {
|
|
2681
|
+
void this.extractAndLearn(inbound.content, channelResponse, session.id);
|
|
2682
|
+
}
|
|
2683
|
+
// Send response (skip if draft streaming already delivered it)
|
|
2684
|
+
if (!draftMessageId && this.channels) {
|
|
2685
|
+
await this.channels.send(inbound.channelType, inbound.channelId, {
|
|
2686
|
+
content: channelResponse,
|
|
2687
|
+
replyToId: inbound.id,
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
audit('message.sent', {
|
|
2691
|
+
channelType: inbound.channelType,
|
|
2692
|
+
sessionId: session.id,
|
|
2693
|
+
inputTokens: channelUsage.inputTokens,
|
|
2694
|
+
outputTokens: channelUsage.outputTokens,
|
|
2695
|
+
});
|
|
2696
|
+
this.agentEnd(channelAgentId, true);
|
|
2697
|
+
}
|
|
2698
|
+
catch (error) {
|
|
2699
|
+
stopTyping();
|
|
2700
|
+
this.agentEnd(channelAgentId, false);
|
|
2701
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
2702
|
+
audit('channel.error', { sessionId: session.id, error: errorMessage });
|
|
2703
|
+
if (this.channels) {
|
|
2704
|
+
await this.channels.send(inbound.channelType, inbound.channelId, {
|
|
2705
|
+
content: `Error: ${errorMessage}`,
|
|
2706
|
+
replyToId: inbound.id,
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}); // end runWithRequestId
|
|
2711
|
+
}
|
|
2712
|
+
async handleChannelCommand(command, sessionId) {
|
|
2713
|
+
const [cmd] = command.slice(1).split(' ');
|
|
2714
|
+
switch (cmd.toLowerCase()) {
|
|
2715
|
+
case 'status': {
|
|
2716
|
+
const activeSessions = this.sessions.getActiveSessions();
|
|
2717
|
+
const providers = this.providers?.listAvailable() || [];
|
|
2718
|
+
const channels = this.channels?.getConnectedChannels() || [];
|
|
2719
|
+
return `**Status**\n- Sessions: ${activeSessions.length} active\n- Providers: ${providers.join(', ') || 'none'}\n- Channels: ${channels.join(', ') || 'webchat only'}`;
|
|
2720
|
+
}
|
|
2721
|
+
case 'new':
|
|
2722
|
+
case 'reset': {
|
|
2723
|
+
await this.sessions.clear(sessionId);
|
|
2724
|
+
return 'Session cleared. Starting fresh!';
|
|
2725
|
+
}
|
|
2726
|
+
case 'help': {
|
|
2727
|
+
return `**Commands**\n- /status - Show system status\n- /new - Start a new session\n- /reset - Clear current session\n- /help - Show this help`;
|
|
2728
|
+
}
|
|
2729
|
+
default:
|
|
2730
|
+
return `Unknown command: ${cmd}. Try /help`;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
/** Register each connector action as an AI-callable tool. */
|
|
2734
|
+
registerConnectorTools() {
|
|
2735
|
+
if (!this.connectorRegistry || !this.connectorAuthManager)
|
|
2736
|
+
return;
|
|
2737
|
+
for (const connector of this.connectorRegistry.list()) {
|
|
2738
|
+
// Only register tools for connectors with active auth tokens
|
|
2739
|
+
if (!this.connectorAuthManager.hasToken(connector.id))
|
|
2740
|
+
continue;
|
|
2741
|
+
for (const action of connector.actions) {
|
|
2742
|
+
const toolName = `${connector.id.replace(/-/g, '_')}_${action.id.replace(/-/g, '_')}`;
|
|
2743
|
+
// Convert connector param schema to tool parameters
|
|
2744
|
+
const parameters = [];
|
|
2745
|
+
for (const [paramName, paramDef] of Object.entries(action.params)) {
|
|
2746
|
+
parameters.push({
|
|
2747
|
+
name: paramName,
|
|
2748
|
+
type: paramDef.type,
|
|
2749
|
+
description: paramDef.description,
|
|
2750
|
+
required: paramDef.required ?? false,
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
const connectorId = connector.id;
|
|
2754
|
+
const actionId = action.id;
|
|
2755
|
+
const authManager = this.connectorAuthManager;
|
|
2756
|
+
toolRegistry.register({
|
|
2757
|
+
name: toolName,
|
|
2758
|
+
description: `[${connector.name}] ${action.description}`,
|
|
2759
|
+
parameters,
|
|
2760
|
+
getPermission: () => {
|
|
2761
|
+
// Trust level 0-1 = require approval, 2+ = auto-approve
|
|
2762
|
+
return action.trustMinimum >= 2
|
|
2763
|
+
? ToolPermission.AUTO_APPROVE
|
|
2764
|
+
: ToolPermission.USER_APPROVAL;
|
|
2765
|
+
},
|
|
2766
|
+
execute: async (params) => {
|
|
2767
|
+
const token = authManager.getToken(connectorId);
|
|
2768
|
+
if (!token) {
|
|
2769
|
+
return { success: false, error: `${connector.name} is not connected. Please authenticate first.` };
|
|
2770
|
+
}
|
|
2771
|
+
try {
|
|
2772
|
+
const result = await connector.executeAction(actionId, params, token.accessToken);
|
|
2773
|
+
return { success: true, output: JSON.stringify(result, null, 2) };
|
|
2774
|
+
}
|
|
2775
|
+
catch (error) {
|
|
2776
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
2777
|
+
}
|
|
2778
|
+
},
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
this.logger.info(`Registered ${connector.actions.length} tools for connector: ${connector.name}`);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
getProviderMaxTokens(provider) {
|
|
2785
|
+
const model = provider.defaultModel;
|
|
2786
|
+
return provider.metadata?.models?.[model]?.maxContextTokens;
|
|
2787
|
+
}
|
|
2788
|
+
async extractAndLearn(userMessage, assistantResponse, sessionId) {
|
|
2789
|
+
try {
|
|
2790
|
+
const recentMessages = this.sessions.getContextMessages(sessionId, this.getProviderMaxTokens(this.providers.getPrimaryProvider()));
|
|
2791
|
+
// AI-powered extraction
|
|
2792
|
+
if (this.memoryExtractor) {
|
|
2793
|
+
const result = await this.memoryExtractor.extract(userMessage, assistantResponse, {
|
|
2794
|
+
messageCount: recentMessages.length,
|
|
2795
|
+
sessionAge: 0, // approximation; session age not easily available here
|
|
2796
|
+
});
|
|
2797
|
+
// Record personality adaptation signals
|
|
2798
|
+
if (this.personalityAdapter && result.personalitySignals.length > 0) {
|
|
2799
|
+
for (const signal of result.personalitySignals) {
|
|
2800
|
+
await this.personalityAdapter.recordSignal(signal);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
const totalExtracted = result.factsExtracted.length +
|
|
2804
|
+
result.patternsDetected.length +
|
|
2805
|
+
result.relationshipsFound.length;
|
|
2806
|
+
if (totalExtracted > 0) {
|
|
2807
|
+
void audit('memory.extracted', { count: totalExtracted });
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
// Run local pattern detection on recent messages
|
|
2811
|
+
if (this.patternDetector && this.memoryStore) {
|
|
2812
|
+
const recent = recentMessages.slice(-20);
|
|
2813
|
+
const patterns = this.patternDetector.detect(recent.map(m => ({
|
|
2814
|
+
content: m.content,
|
|
2815
|
+
role: m.role,
|
|
2816
|
+
timestamp: m.timestamp,
|
|
2817
|
+
})));
|
|
2818
|
+
for (const pattern of patterns) {
|
|
2819
|
+
await this.memoryStore.add(pattern.pattern, 'pattern', 'observed', {
|
|
2820
|
+
confidence: pattern.confidence,
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
catch (error) {
|
|
2826
|
+
// Silent failure — don't block the response
|
|
2827
|
+
this.logger.warn('Memory extraction failed', { error: error instanceof Error ? error : new Error(String(error)) });
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
async getLivingMemoryState() {
|
|
2831
|
+
if (!this.memoryStore)
|
|
2832
|
+
return null;
|
|
2833
|
+
const all = await this.memoryStore.getAll();
|
|
2834
|
+
const stats = await this.memoryStore.getStats();
|
|
2835
|
+
return {
|
|
2836
|
+
facts: all.filter(m => ['preference', 'fact', 'context'].includes(m.category)),
|
|
2837
|
+
relationships: all.filter(m => m.category === 'relationship'),
|
|
2838
|
+
patterns: all.filter(m => m.category === 'pattern'),
|
|
2839
|
+
adaptations: this.personalityAdapter ? await this.personalityAdapter.getAdjustments() : [],
|
|
2840
|
+
stats,
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
createWebhookRouter() {
|
|
2844
|
+
const router = Router();
|
|
2845
|
+
// Generic webhooks
|
|
2846
|
+
router.post('/custom/:name', async (req, res) => {
|
|
2847
|
+
if (!this.webhookManager) {
|
|
2848
|
+
res.status(503).json({ error: 'Webhooks not available' });
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
// Collect raw body
|
|
2852
|
+
const chunks = [];
|
|
2853
|
+
for await (const chunk of req) {
|
|
2854
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
2855
|
+
}
|
|
2856
|
+
const body = Buffer.concat(chunks);
|
|
2857
|
+
const name = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
|
|
2858
|
+
const result = await this.webhookManager.handleGenericWebhook(name, body, req.headers);
|
|
2859
|
+
res.status(result.status).json({
|
|
2860
|
+
accepted: result.accepted,
|
|
2861
|
+
...(result.error && !result.accepted ? { error: result.error } : {}),
|
|
2862
|
+
});
|
|
2863
|
+
});
|
|
2864
|
+
// Channel webhooks — Twilio
|
|
2865
|
+
router.post('/twilio', async (req, res) => {
|
|
2866
|
+
if (!this.channels) {
|
|
2867
|
+
res.status(503).json({ error: 'Channels not available' });
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const adapter = this.channels.getAdapter('twilio');
|
|
2871
|
+
if (!adapter) {
|
|
2872
|
+
res.status(503).json({ error: 'Twilio not configured' });
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
const twilioAdapter = adapter;
|
|
2876
|
+
await twilioAdapter.handleWebhook(req.body);
|
|
2877
|
+
res.status(200).type('text/xml').send('<Response></Response>');
|
|
2878
|
+
});
|
|
2879
|
+
// Channel webhooks — Telegram
|
|
2880
|
+
router.post('/telegram', async (req, res) => {
|
|
2881
|
+
if (!this.channels) {
|
|
2882
|
+
res.status(503).json({ error: 'Channels not available' });
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const adapter = this.channels.getAdapter('telegram');
|
|
2886
|
+
if (!adapter) {
|
|
2887
|
+
res.status(503).json({ error: 'Telegram not configured' });
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
const telegramAdapter = adapter;
|
|
2891
|
+
await telegramAdapter.handleWebhook(req.body);
|
|
2892
|
+
res.sendStatus(200);
|
|
2893
|
+
});
|
|
2894
|
+
return router;
|
|
2895
|
+
}
|
|
2896
|
+
async start() {
|
|
2897
|
+
if (this.running)
|
|
2898
|
+
return;
|
|
2899
|
+
await this.gateway.start();
|
|
2900
|
+
// Connect channels if configured
|
|
2901
|
+
if (this.channels) {
|
|
2902
|
+
try {
|
|
2903
|
+
await this.channels.connectAll();
|
|
2904
|
+
const connected = this.channels.getConnectedChannels();
|
|
2905
|
+
if (connected.length > 0) {
|
|
2906
|
+
this.logger.info(`Connected channels: ${connected.join(', ')}`);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
catch (error) {
|
|
2910
|
+
this.logger.warn('Some channels failed to connect', { error: error instanceof Error ? error : new Error(String(error)) });
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
// Load persisted channel targets for proactive delivery (behaviors, ambient)
|
|
2914
|
+
await this.loadChannelTargets();
|
|
2915
|
+
this.running = true;
|
|
2916
|
+
console.log(`\n${this.getAgentName()} is ready!`);
|
|
2917
|
+
console.log(`Open http://${this.config.gateway.host}:${this.config.gateway.port} in your browser\n`);
|
|
2918
|
+
}
|
|
2919
|
+
async stop() {
|
|
2920
|
+
if (!this.running)
|
|
2921
|
+
return;
|
|
2922
|
+
if (this.channels) {
|
|
2923
|
+
await this.channels.disconnectAll();
|
|
2924
|
+
}
|
|
2925
|
+
await this.gateway.stop();
|
|
2926
|
+
if (this.behaviors) {
|
|
2927
|
+
await this.behaviors.stop();
|
|
2928
|
+
}
|
|
2929
|
+
if (this.browserManager) {
|
|
2930
|
+
await this.browserManager.shutdown();
|
|
2931
|
+
}
|
|
2932
|
+
if (this.voiceManager) {
|
|
2933
|
+
await this.voiceManager.shutdown();
|
|
2934
|
+
}
|
|
2935
|
+
if (this.pluginLoader) {
|
|
2936
|
+
await this.pluginLoader.shutdownAll();
|
|
2937
|
+
}
|
|
2938
|
+
if (this.ambientScheduler) {
|
|
2939
|
+
this.ambientScheduler.stop();
|
|
2940
|
+
}
|
|
2941
|
+
if (this.autonomousExecutor) {
|
|
2942
|
+
this.autonomousExecutor.stop();
|
|
2943
|
+
}
|
|
2944
|
+
if (this.memoryCleanupInterval) {
|
|
2945
|
+
clearInterval(this.memoryCleanupInterval);
|
|
2946
|
+
this.memoryCleanupInterval = undefined;
|
|
2947
|
+
}
|
|
2948
|
+
this.sessions.destroy();
|
|
2949
|
+
this.vault.lock();
|
|
2950
|
+
this.running = false;
|
|
2951
|
+
}
|
|
2952
|
+
getConfig() {
|
|
2953
|
+
return this.config;
|
|
2954
|
+
}
|
|
2955
|
+
getAgentName() {
|
|
2956
|
+
return this.config.agent?.name ?? 'Auxiora';
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
function deepMerge(target, source) {
|
|
2960
|
+
const result = { ...target };
|
|
2961
|
+
for (const key of Object.keys(source)) {
|
|
2962
|
+
const srcVal = source[key];
|
|
2963
|
+
const tgtVal = target[key];
|
|
2964
|
+
if (srcVal && tgtVal && typeof srcVal === 'object' && typeof tgtVal === 'object' && !Array.isArray(srcVal)) {
|
|
2965
|
+
result[key] = deepMerge(tgtVal, srcVal);
|
|
2966
|
+
}
|
|
2967
|
+
else {
|
|
2968
|
+
result[key] = srcVal;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
return result;
|
|
2972
|
+
}
|
|
2973
|
+
export { AgentRouter, AgentInstance, } from './agent-router.js';
|
|
2974
|
+
export { BlockStream, ToolOutputStream, } from './block-stream.js';
|
|
2975
|
+
export async function startAuxiora(options = {}) {
|
|
2976
|
+
const auxiora = new Auxiora();
|
|
2977
|
+
await auxiora.initialize(options);
|
|
2978
|
+
await auxiora.start();
|
|
2979
|
+
return auxiora;
|
|
2980
|
+
}
|
|
2981
|
+
//# sourceMappingURL=index.js.map
|