@usejarvis/brain 0.1.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 +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Agent Service — Independent Monitoring Brain
|
|
3
|
+
*
|
|
4
|
+
* Runs heartbeats, event reactions, and commitment executions on a
|
|
5
|
+
* SEPARATE agent with its own browser instance (CDP port 9223).
|
|
6
|
+
* User chat on the main AgentService is never blocked.
|
|
7
|
+
*
|
|
8
|
+
* Shares: LLMManager (same API keys), SQLite vault (same DB)
|
|
9
|
+
* Separate: BrowserController, AgentOrchestrator, ToolRegistry, conversation history
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import type { Service, ServiceStatus } from './services.ts';
|
|
15
|
+
import type { IAgentService } from './agent-service-interface.ts';
|
|
16
|
+
import type { JarvisConfig } from '../config/types.ts';
|
|
17
|
+
import type { RoleDefinition } from '../roles/types.ts';
|
|
18
|
+
import type { LLMManager } from '../llm/manager.ts';
|
|
19
|
+
import type { ResearchQueue } from './research-queue.ts';
|
|
20
|
+
|
|
21
|
+
import { AgentOrchestrator } from '../agents/orchestrator.ts';
|
|
22
|
+
import { loadRole } from '../roles/loader.ts';
|
|
23
|
+
import { ToolRegistry } from '../actions/tools/registry.ts';
|
|
24
|
+
import { NON_BROWSER_TOOLS, createBrowserTools } from '../actions/tools/builtin.ts';
|
|
25
|
+
import { BrowserController } from '../actions/browser/session.ts';
|
|
26
|
+
import { DESKTOP_TOOLS } from '../actions/tools/desktop.ts';
|
|
27
|
+
import { commitmentsTool } from '../actions/tools/commitments.ts';
|
|
28
|
+
import { researchQueueTool } from '../actions/tools/research.ts';
|
|
29
|
+
import { buildSystemPrompt, type PromptContext } from '../roles/prompt-builder.ts';
|
|
30
|
+
import { getDueCommitments, getUpcoming } from '../vault/commitments.ts';
|
|
31
|
+
import { getRecentObservations } from '../vault/observations.ts';
|
|
32
|
+
import { findContent } from '../vault/content-pipeline.ts';
|
|
33
|
+
import { getRecentConversation, getMessages } from '../vault/conversations.ts';
|
|
34
|
+
import { getActiveGoalsSummary } from '../vault/retrieval.ts';
|
|
35
|
+
|
|
36
|
+
const BG_CDP_PORT = 9223;
|
|
37
|
+
const BG_PROFILE_DIR = join(homedir(), '.jarvis', 'browser', 'bg-profile');
|
|
38
|
+
|
|
39
|
+
export class BackgroundAgentService implements Service, IAgentService {
|
|
40
|
+
name = 'background-agent';
|
|
41
|
+
private _status: ServiceStatus = 'stopped';
|
|
42
|
+
private config: JarvisConfig;
|
|
43
|
+
private llmManager: LLMManager;
|
|
44
|
+
private orchestrator: AgentOrchestrator;
|
|
45
|
+
private bgBrowser: BrowserController;
|
|
46
|
+
private role: RoleDefinition | null = null;
|
|
47
|
+
private researchQueue: ResearchQueue | null = null;
|
|
48
|
+
private busy = false;
|
|
49
|
+
|
|
50
|
+
constructor(config: JarvisConfig, llmManager: LLMManager) {
|
|
51
|
+
this.config = config;
|
|
52
|
+
this.llmManager = llmManager;
|
|
53
|
+
this.orchestrator = new AgentOrchestrator();
|
|
54
|
+
this.bgBrowser = new BrowserController(BG_CDP_PORT, BG_PROFILE_DIR);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setResearchQueue(queue: ResearchQueue): void {
|
|
58
|
+
this.researchQueue = queue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async start(): Promise<void> {
|
|
62
|
+
this._status = 'starting';
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// 1. Wire shared LLM manager
|
|
66
|
+
this.orchestrator.setLLMManager(this.llmManager);
|
|
67
|
+
|
|
68
|
+
// 2. Load the same role as the main agent
|
|
69
|
+
this.role = this.loadActiveRole();
|
|
70
|
+
|
|
71
|
+
// 3. Build tool registry with background browser
|
|
72
|
+
const toolRegistry = new ToolRegistry();
|
|
73
|
+
|
|
74
|
+
for (const tool of NON_BROWSER_TOOLS) {
|
|
75
|
+
toolRegistry.register(tool);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const bgBrowserTools = createBrowserTools(this.bgBrowser);
|
|
79
|
+
for (const tool of bgBrowserTools) {
|
|
80
|
+
toolRegistry.register(tool);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Desktop tools (routed via sidecar RPC)
|
|
84
|
+
for (const tool of DESKTOP_TOOLS) {
|
|
85
|
+
toolRegistry.register(tool);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
toolRegistry.register(commitmentsTool);
|
|
89
|
+
toolRegistry.register(researchQueueTool);
|
|
90
|
+
|
|
91
|
+
this.orchestrator.setToolRegistry(toolRegistry);
|
|
92
|
+
|
|
93
|
+
// 4. Create primary agent for background operations
|
|
94
|
+
this.orchestrator.createPrimary(this.role);
|
|
95
|
+
|
|
96
|
+
this._status = 'running';
|
|
97
|
+
console.log(`[BackgroundAgent] Started with role: ${this.role.name}, browser on port ${BG_CDP_PORT}`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this._status = 'error';
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async stop(): Promise<void> {
|
|
105
|
+
this._status = 'stopping';
|
|
106
|
+
const primary = this.orchestrator.getPrimary();
|
|
107
|
+
if (primary) {
|
|
108
|
+
this.orchestrator.terminateAgent(primary.id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.bgBrowser.connected) {
|
|
112
|
+
await this.bgBrowser.disconnect();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this._status = 'stopped';
|
|
116
|
+
console.log('[BackgroundAgent] Stopped');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
status(): ServiceStatus {
|
|
120
|
+
return this._status;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get isBusy(): boolean {
|
|
124
|
+
return this.busy;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Handle periodic heartbeat with full tool access.
|
|
129
|
+
* Returns null if busy (non-blocking for the caller).
|
|
130
|
+
*/
|
|
131
|
+
async handleHeartbeat(coalescedEvents?: string): Promise<string | null> {
|
|
132
|
+
if (this.busy) {
|
|
133
|
+
console.log('[BackgroundAgent] Skipping heartbeat — already busy');
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.busy = true;
|
|
138
|
+
try {
|
|
139
|
+
const systemPrompt = this.buildHeartbeatPrompt(coalescedEvents);
|
|
140
|
+
const parts: string[] = ['[HEARTBEAT] Periodic check-in. Review your responsibilities and take action.'];
|
|
141
|
+
if (coalescedEvents) {
|
|
142
|
+
parts.push('', coalescedEvents);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const response = await this.orchestrator.processMessage(systemPrompt, parts.join('\n'));
|
|
146
|
+
if (response && response.trim().length > 0) {
|
|
147
|
+
return response;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('[BackgroundAgent] Heartbeat error:', err);
|
|
152
|
+
return null;
|
|
153
|
+
} finally {
|
|
154
|
+
this.busy = false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle a reactive event message (from EventReactor / CommitmentExecutor).
|
|
160
|
+
*/
|
|
161
|
+
async handleMessage(text: string, channel: string = 'system'): Promise<string> {
|
|
162
|
+
// Wait if busy — event reactor already has its own queue, so this is a safety net
|
|
163
|
+
const waitStart = Date.now();
|
|
164
|
+
while (this.busy && Date.now() - waitStart < 60_000) {
|
|
165
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.busy = true;
|
|
169
|
+
try {
|
|
170
|
+
const systemPrompt = this.buildSystemPrompt(channel);
|
|
171
|
+
return await this.orchestrator.processMessage(systemPrompt, text);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error('[BackgroundAgent] Message error:', err);
|
|
174
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
175
|
+
} finally {
|
|
176
|
+
this.busy = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Private methods ---
|
|
181
|
+
|
|
182
|
+
private buildSystemPrompt(channel: string): string {
|
|
183
|
+
if (!this.role) return '';
|
|
184
|
+
const context = this.buildPromptContext();
|
|
185
|
+
return buildSystemPrompt(this.role, context);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get the last N messages from the most recent chat conversation.
|
|
190
|
+
* Returns formatted chat transcript and staleness info.
|
|
191
|
+
*/
|
|
192
|
+
private getRecentChatContext(messageCount: number = 20): {
|
|
193
|
+
transcript: string | null;
|
|
194
|
+
lastUserMessageAt: number | null;
|
|
195
|
+
lastAssistantMessageAt: number | null;
|
|
196
|
+
minutesSinceLastUserMessage: number | null;
|
|
197
|
+
} {
|
|
198
|
+
try {
|
|
199
|
+
const recent = getRecentConversation('websocket');
|
|
200
|
+
if (!recent) return { transcript: null, lastUserMessageAt: null, lastAssistantMessageAt: null, minutesSinceLastUserMessage: null };
|
|
201
|
+
|
|
202
|
+
const messages = getMessages(recent.conversation.id, { limit: messageCount });
|
|
203
|
+
if (messages.length === 0) return { transcript: null, lastUserMessageAt: null, lastAssistantMessageAt: null, minutesSinceLastUserMessage: null };
|
|
204
|
+
|
|
205
|
+
// Find timestamps for staleness detection
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
let lastUserAt: number | null = null;
|
|
208
|
+
let lastAssistantAt: number | null = null;
|
|
209
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
210
|
+
const msg = messages[i]!;
|
|
211
|
+
if (!lastUserAt && msg.role === 'user') lastUserAt = msg.created_at;
|
|
212
|
+
if (!lastAssistantAt && msg.role === 'assistant') lastAssistantAt = msg.created_at;
|
|
213
|
+
if (lastUserAt && lastAssistantAt) break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Format transcript
|
|
217
|
+
const lines = messages
|
|
218
|
+
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
219
|
+
.map(m => {
|
|
220
|
+
const time = new Date(m.created_at).toLocaleTimeString();
|
|
221
|
+
const role = m.role === 'user' ? 'USER' : 'JARVIS';
|
|
222
|
+
// Truncate long messages to keep context manageable
|
|
223
|
+
const content = m.content.length > 500 ? m.content.slice(0, 500) + '...' : m.content;
|
|
224
|
+
return `[${time}] ${role}: ${content}`;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
transcript: lines.join('\n'),
|
|
229
|
+
lastUserMessageAt: lastUserAt,
|
|
230
|
+
lastAssistantMessageAt: lastAssistantAt,
|
|
231
|
+
minutesSinceLastUserMessage: lastUserAt ? Math.round((now - lastUserAt) / 60_000) : null,
|
|
232
|
+
};
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error('[BackgroundAgent] Error loading chat context:', err);
|
|
235
|
+
return { transcript: null, lastUserMessageAt: null, lastAssistantMessageAt: null, minutesSinceLastUserMessage: null };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private buildHeartbeatPrompt(coalescedEvents?: string): string {
|
|
240
|
+
if (!this.role) return '';
|
|
241
|
+
|
|
242
|
+
const context = this.buildPromptContext();
|
|
243
|
+
const rolePrompt = buildSystemPrompt(this.role, context);
|
|
244
|
+
|
|
245
|
+
const parts = [rolePrompt, '', '# Heartbeat Check', this.role.heartbeat_instructions];
|
|
246
|
+
|
|
247
|
+
// --- RECENT CHAT CONTEXT ---
|
|
248
|
+
const chat = this.getRecentChatContext(20);
|
|
249
|
+
if (chat.transcript) {
|
|
250
|
+
parts.push('', '# RECENT CHAT (last 20 messages)');
|
|
251
|
+
parts.push('Review this conversation for unfulfilled promises, unanswered questions, or implicit commitments.');
|
|
252
|
+
parts.push('');
|
|
253
|
+
parts.push(chat.transcript);
|
|
254
|
+
|
|
255
|
+
// Staleness warning
|
|
256
|
+
if (chat.minutesSinceLastUserMessage !== null && chat.minutesSinceLastUserMessage >= 120) {
|
|
257
|
+
parts.push('');
|
|
258
|
+
parts.push(`⚠ CONVERSATION STALE: Last user message was ${chat.minutesSinceLastUserMessage} minutes ago.`);
|
|
259
|
+
parts.push('Consider a gentle proactive check-in if appropriate during active hours.');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Detect if JARVIS was last to speak (may have promised something)
|
|
263
|
+
if (chat.lastAssistantMessageAt && chat.lastUserMessageAt && chat.lastAssistantMessageAt > chat.lastUserMessageAt) {
|
|
264
|
+
parts.push('');
|
|
265
|
+
parts.push('NOTE: JARVIS was the last to speak. Check if that last message contained any promises, "I\'ll do X" statements, or tasks that may not have been completed.');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- ACTIVE GOALS ---
|
|
270
|
+
try {
|
|
271
|
+
const goalsSummary = getActiveGoalsSummary();
|
|
272
|
+
if (goalsSummary) {
|
|
273
|
+
parts.push('', '# ACTIVE GOALS');
|
|
274
|
+
parts.push('Cross-reference these with the recent chat. If goals were discussed but not updated, flag it.');
|
|
275
|
+
parts.push('');
|
|
276
|
+
parts.push(goalsSummary);
|
|
277
|
+
}
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error('[BackgroundAgent] Error loading goals summary:', err);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (coalescedEvents) {
|
|
283
|
+
parts.push('', '# Recent System Events', coalescedEvents);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
parts.push('', '# COMMITMENT EXECUTION');
|
|
287
|
+
parts.push('If any commitments are overdue or due soon, EXECUTE them now using your tools.');
|
|
288
|
+
parts.push('Do not just mention them — actually perform the work. Use browse, terminal, file operations as needed.');
|
|
289
|
+
|
|
290
|
+
if (this.researchQueue && this.researchQueue.queuedCount() > 0) {
|
|
291
|
+
const next = this.researchQueue.getNext();
|
|
292
|
+
if (next) {
|
|
293
|
+
parts.push('', '# BACKGROUND RESEARCH');
|
|
294
|
+
parts.push(`You have a research topic queued: "${next.topic}"`);
|
|
295
|
+
parts.push(`Reason: ${next.reason}`);
|
|
296
|
+
parts.push(`Research ID: ${next.id}`);
|
|
297
|
+
parts.push('If nothing urgent needs your attention, research this topic now.');
|
|
298
|
+
parts.push('Use your browser and tools to gather information, then use the research_queue tool with action "complete" to save your findings.');
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
parts.push('', '# IDLE MODE');
|
|
302
|
+
parts.push('No research topics queued. If nothing urgent, you may:');
|
|
303
|
+
parts.push('- Check news or trends relevant to the user');
|
|
304
|
+
parts.push('- Review and organize pending tasks');
|
|
305
|
+
parts.push('- Or simply report "All clear" if nothing needs attention');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
parts.push('', '# Important', 'You have full tool access during this heartbeat. If you need to take action (browse the web, run commands, check files), DO IT. Be proactive and aggressive about helping.');
|
|
309
|
+
|
|
310
|
+
return parts.join('\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private buildPromptContext(): PromptContext {
|
|
314
|
+
const context: PromptContext = {
|
|
315
|
+
currentTime: new Date().toISOString(),
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Get due commitments
|
|
319
|
+
try {
|
|
320
|
+
const due = getDueCommitments();
|
|
321
|
+
const upcoming = getUpcoming(5);
|
|
322
|
+
const allCommitments = [...due, ...upcoming];
|
|
323
|
+
|
|
324
|
+
if (allCommitments.length > 0) {
|
|
325
|
+
context.activeCommitments = allCommitments.map((c) => {
|
|
326
|
+
const dueStr = c.when_due
|
|
327
|
+
? ` (due: ${new Date(c.when_due).toLocaleString()})`
|
|
328
|
+
: '';
|
|
329
|
+
return `[${c.priority}] ${c.what}${dueStr} — ${c.status}`;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.error('[BackgroundAgent] Error loading commitments:', err);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Get active content pipeline items
|
|
337
|
+
try {
|
|
338
|
+
const activeContent = findContent({}).filter(
|
|
339
|
+
(c) => c.stage !== 'published'
|
|
340
|
+
).slice(0, 10);
|
|
341
|
+
if (activeContent.length > 0) {
|
|
342
|
+
context.contentPipeline = activeContent.map((c) => {
|
|
343
|
+
const tags = c.tags.length > 0 ? ` [${c.tags.join(', ')}]` : '';
|
|
344
|
+
return `"${c.title}" (${c.content_type}) — ${c.stage}${tags}`;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error('[BackgroundAgent] Error loading content pipeline:', err);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Get recent observations
|
|
352
|
+
try {
|
|
353
|
+
const observations = getRecentObservations(undefined, 10);
|
|
354
|
+
if (observations.length > 0) {
|
|
355
|
+
context.recentObservations = observations.map((o) => {
|
|
356
|
+
const time = new Date(o.created_at).toLocaleTimeString();
|
|
357
|
+
return `[${time}] ${o.type}: ${JSON.stringify(o.data).slice(0, 200)}`;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error('[BackgroundAgent] Error loading observations:', err);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return context;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private loadActiveRole(): RoleDefinition {
|
|
368
|
+
const roleName = this.config.active_role;
|
|
369
|
+
|
|
370
|
+
// Package-root-relative paths for global install compatibility
|
|
371
|
+
const pkgRoot = join(import.meta.dir, '../..');
|
|
372
|
+
const paths = [
|
|
373
|
+
join(pkgRoot, `roles/${roleName}.yaml`),
|
|
374
|
+
join(pkgRoot, `roles/${roleName}.yml`),
|
|
375
|
+
join(pkgRoot, `config/roles/${roleName}.yaml`),
|
|
376
|
+
join(pkgRoot, `config/roles/${roleName}.yml`),
|
|
377
|
+
// Also try CWD-relative for local dev
|
|
378
|
+
`roles/${roleName}.yaml`,
|
|
379
|
+
`roles/${roleName}.yml`,
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
for (const rolePath of paths) {
|
|
383
|
+
try {
|
|
384
|
+
const role = loadRole(rolePath);
|
|
385
|
+
console.log(`[BackgroundAgent] Loaded role '${role.name}' from ${rolePath}`);
|
|
386
|
+
return role;
|
|
387
|
+
} catch {
|
|
388
|
+
// Try next path
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
throw new Error(
|
|
393
|
+
`[BackgroundAgent] Could not load role '${roleName}'. Searched: ${paths.join(', ')}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test';
|
|
2
|
+
import { createBrowserTools, NON_BROWSER_TOOLS, BUILTIN_TOOLS } from '../actions/tools/builtin.ts';
|
|
3
|
+
import { BrowserController } from '../actions/browser/session.ts';
|
|
4
|
+
|
|
5
|
+
describe('NON_BROWSER_TOOLS', () => {
|
|
6
|
+
test('contains 9 non-browser tools', () => {
|
|
7
|
+
expect(NON_BROWSER_TOOLS).toHaveLength(9);
|
|
8
|
+
const names = NON_BROWSER_TOOLS.map(t => t.name);
|
|
9
|
+
expect(names).toContain('run_command');
|
|
10
|
+
expect(names).toContain('read_file');
|
|
11
|
+
expect(names).toContain('write_file');
|
|
12
|
+
expect(names).toContain('list_directory');
|
|
13
|
+
expect(names).toContain('list_sidecars');
|
|
14
|
+
expect(names).toContain('get_clipboard');
|
|
15
|
+
expect(names).toContain('set_clipboard');
|
|
16
|
+
expect(names).toContain('capture_screen');
|
|
17
|
+
expect(names).toContain('get_system_info');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('none have browser category', () => {
|
|
21
|
+
for (const tool of NON_BROWSER_TOOLS) {
|
|
22
|
+
expect(tool.category).not.toBe('browser');
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('createBrowserTools', () => {
|
|
28
|
+
test('returns 7 browser tools', () => {
|
|
29
|
+
const ctrl = new BrowserController(9999);
|
|
30
|
+
const tools = createBrowserTools(ctrl);
|
|
31
|
+
expect(tools).toHaveLength(7);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('all tools have browser category', () => {
|
|
35
|
+
const ctrl = new BrowserController(9999);
|
|
36
|
+
const tools = createBrowserTools(ctrl);
|
|
37
|
+
for (const tool of tools) {
|
|
38
|
+
expect(tool.category).toBe('browser');
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('tool names match expected browser tools', () => {
|
|
43
|
+
const ctrl = new BrowserController(9999);
|
|
44
|
+
const tools = createBrowserTools(ctrl);
|
|
45
|
+
const names = tools.map(t => t.name).sort();
|
|
46
|
+
expect(names).toEqual([
|
|
47
|
+
'browser_click',
|
|
48
|
+
'browser_evaluate',
|
|
49
|
+
'browser_navigate',
|
|
50
|
+
'browser_screenshot',
|
|
51
|
+
'browser_scroll',
|
|
52
|
+
'browser_snapshot',
|
|
53
|
+
'browser_type',
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('BUILTIN_TOOLS = NON_BROWSER_TOOLS + 7 browser + 9 desktop tools', () => {
|
|
58
|
+
expect(BUILTIN_TOOLS).toHaveLength(NON_BROWSER_TOOLS.length + 7 + 9);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('BrowserController parameterization', () => {
|
|
63
|
+
test('accepts custom port', () => {
|
|
64
|
+
const ctrl = new BrowserController(9223);
|
|
65
|
+
// Should not throw — port is stored internally
|
|
66
|
+
expect(ctrl).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('accepts custom port and profile dir', () => {
|
|
70
|
+
const ctrl = new BrowserController(9223, '/tmp/test-bg-profile');
|
|
71
|
+
expect(ctrl).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('defaults work (no args)', () => {
|
|
75
|
+
const ctrl = new BrowserController();
|
|
76
|
+
expect(ctrl).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel Service — External Communication Channels
|
|
3
|
+
*
|
|
4
|
+
* Manages Telegram, Discord (and future) channel adapters.
|
|
5
|
+
* Routes all external messages through the same AgentService (same brain),
|
|
6
|
+
* persists conversations to the vault (unified history), and handles
|
|
7
|
+
* proactive broadcasts to all connected channels.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Service, ServiceStatus } from './services.ts';
|
|
11
|
+
import type { AgentService } from './agent-service.ts';
|
|
12
|
+
import type { JarvisConfig } from '../config/types.ts';
|
|
13
|
+
import type { ChannelMessage } from '../comms/channels/telegram.ts';
|
|
14
|
+
import type { STTProvider } from '../comms/voice.ts';
|
|
15
|
+
|
|
16
|
+
import { ChannelManager } from '../comms/index.ts';
|
|
17
|
+
import { TelegramAdapter } from '../comms/channels/telegram.ts';
|
|
18
|
+
import { DiscordAdapter } from '../comms/channels/discord.ts';
|
|
19
|
+
import { createSTTProvider } from '../comms/voice.ts';
|
|
20
|
+
import { getOrCreateConversation, addMessage } from '../vault/conversations.ts';
|
|
21
|
+
|
|
22
|
+
export type ApprovalCommandHandler = (action: 'approve' | 'deny', shortId: string, channel: string) => Promise<string>;
|
|
23
|
+
|
|
24
|
+
export class ChannelService implements Service {
|
|
25
|
+
name = 'channels';
|
|
26
|
+
private _status: ServiceStatus = 'stopped';
|
|
27
|
+
private config: JarvisConfig;
|
|
28
|
+
private agentService: AgentService;
|
|
29
|
+
private manager: ChannelManager;
|
|
30
|
+
private sttProvider: STTProvider | null = null;
|
|
31
|
+
/** Track last message sender per channel for proactive broadcasts */
|
|
32
|
+
private lastRecipients = new Map<string, string>();
|
|
33
|
+
/** Handler for approval commands (approve/deny) from external channels */
|
|
34
|
+
private approvalHandler: ApprovalCommandHandler | null = null;
|
|
35
|
+
|
|
36
|
+
constructor(config: JarvisConfig, agentService: AgentService) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.agentService = agentService;
|
|
39
|
+
this.manager = new ChannelManager();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setApprovalHandler(handler: ApprovalCommandHandler): void {
|
|
43
|
+
this.approvalHandler = handler;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async start(): Promise<void> {
|
|
47
|
+
this._status = 'starting';
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// 1. Create STT provider if configured
|
|
51
|
+
if (this.config.stt) {
|
|
52
|
+
this.sttProvider = createSTTProvider(this.config.stt);
|
|
53
|
+
if (this.sttProvider) {
|
|
54
|
+
console.log(`[ChannelService] STT provider: ${this.config.stt.provider}`);
|
|
55
|
+
} else {
|
|
56
|
+
console.log('[ChannelService] STT configured but no valid credentials — voice messages disabled');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. Create & register adapters from config
|
|
61
|
+
const channels = this.config.channels;
|
|
62
|
+
|
|
63
|
+
if (channels?.telegram?.enabled && channels.telegram.bot_token) {
|
|
64
|
+
const telegram = new TelegramAdapter(channels.telegram.bot_token, {
|
|
65
|
+
sttProvider: this.sttProvider ?? undefined,
|
|
66
|
+
allowedUsers: channels.telegram.allowed_users,
|
|
67
|
+
});
|
|
68
|
+
this.manager.register(telegram);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (channels?.discord?.enabled && channels.discord.bot_token) {
|
|
72
|
+
const discord = new DiscordAdapter(channels.discord.bot_token, {
|
|
73
|
+
sttProvider: this.sttProvider ?? undefined,
|
|
74
|
+
allowedUsers: channels.discord.allowed_users,
|
|
75
|
+
guildId: channels.discord.guild_id,
|
|
76
|
+
});
|
|
77
|
+
this.manager.register(discord);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Set unified message handler — same brain for all channels
|
|
81
|
+
this.manager.setHandler(async (msg: ChannelMessage): Promise<string> => {
|
|
82
|
+
return this.handleChannelMessage(msg);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 4. Connect all registered channels (Promise.allSettled — one failure doesn't block others)
|
|
86
|
+
const channelList = this.manager.listChannels();
|
|
87
|
+
if (channelList.length > 0) {
|
|
88
|
+
await this.manager.connectAll();
|
|
89
|
+
console.log(`[ChannelService] Active channels: ${channelList.join(', ')}`);
|
|
90
|
+
} else {
|
|
91
|
+
console.log('[ChannelService] No channels configured — enable in Dashboard Settings or config.yaml');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this._status = 'running';
|
|
95
|
+
console.log('[ChannelService] Started');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
this._status = 'error';
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async stop(): Promise<void> {
|
|
103
|
+
this._status = 'stopping';
|
|
104
|
+
await this.manager.disconnectAll();
|
|
105
|
+
this._status = 'stopped';
|
|
106
|
+
console.log('[ChannelService] Stopped');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
status(): ServiceStatus {
|
|
110
|
+
return this._status;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Expose manager for direct adapter access if needed */
|
|
114
|
+
getManager(): ChannelManager {
|
|
115
|
+
return this.manager;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Get connection status of all channels */
|
|
119
|
+
getChannelStatus(): Record<string, boolean> {
|
|
120
|
+
return this.manager.getStatus();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Send a message to a specific channel.
|
|
125
|
+
* Used for targeted proactive notifications.
|
|
126
|
+
*/
|
|
127
|
+
async sendToChannel(channelName: string, recipientId: string, text: string): Promise<void> {
|
|
128
|
+
const adapter = this.manager.getChannel(channelName);
|
|
129
|
+
if (!adapter || !adapter.isConnected()) {
|
|
130
|
+
console.warn(`[ChannelService] Cannot send to ${channelName}: not connected`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
await adapter.sendMessage(recipientId, text);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error(`[ChannelService] Failed to send to ${channelName}:`, err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Broadcast a message to ALL connected external channels.
|
|
142
|
+
* Uses the last known recipient per channel (from most recent inbound message).
|
|
143
|
+
*/
|
|
144
|
+
async broadcastToAll(text: string): Promise<void> {
|
|
145
|
+
for (const name of this.manager.listChannels()) {
|
|
146
|
+
const adapter = this.manager.getChannel(name);
|
|
147
|
+
if (!adapter?.isConnected()) continue;
|
|
148
|
+
|
|
149
|
+
const lastRecipient = this.lastRecipients.get(name);
|
|
150
|
+
if (!lastRecipient) {
|
|
151
|
+
console.log(`[ChannelService] No known recipient for ${name}, skipping broadcast`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await adapter.sendMessage(lastRecipient, text);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error(`[ChannelService] Broadcast to ${name} failed:`, err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Core message handler: receives from any channel, routes to AgentService,
|
|
165
|
+
* persists to vault (unified history), returns response.
|
|
166
|
+
*/
|
|
167
|
+
private async handleChannelMessage(msg: ChannelMessage): Promise<string> {
|
|
168
|
+
const channelTag = msg.channel; // 'telegram' | 'discord'
|
|
169
|
+
|
|
170
|
+
// Track recipient for future broadcasts
|
|
171
|
+
const recipientId = String(msg.metadata.chatId ?? msg.metadata.channelId ?? msg.from);
|
|
172
|
+
this.lastRecipients.set(channelTag, recipientId);
|
|
173
|
+
|
|
174
|
+
// Check for approval commands: "approve <id>" or "deny <id>"
|
|
175
|
+
const trimmed = msg.text.trim().toLowerCase();
|
|
176
|
+
const approveMatch = trimmed.match(/^approve\s+([a-f0-9-]+)/i);
|
|
177
|
+
const denyMatch = trimmed.match(/^deny\s+([a-f0-9-]+)/i);
|
|
178
|
+
|
|
179
|
+
if (this.approvalHandler && (approveMatch || denyMatch)) {
|
|
180
|
+
const action = approveMatch ? 'approve' : 'deny';
|
|
181
|
+
const shortId = (approveMatch ?? denyMatch)![1];
|
|
182
|
+
try {
|
|
183
|
+
return await this.approvalHandler(action as 'approve' | 'deny', shortId!, channelTag);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return `Error processing approval: ${err instanceof Error ? err.message : String(err)}`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 1. Persist inbound user message to vault
|
|
190
|
+
const conversation = getOrCreateConversation(channelTag);
|
|
191
|
+
addMessage(conversation.id, { role: 'user', content: msg.text });
|
|
192
|
+
|
|
193
|
+
// 2. Route to AgentService (non-streaming — external channels are request/response)
|
|
194
|
+
const response = await this.agentService.handleMessage(msg.text, channelTag);
|
|
195
|
+
|
|
196
|
+
// 3. Persist assistant response to vault
|
|
197
|
+
addMessage(conversation.id, { role: 'assistant', content: response });
|
|
198
|
+
|
|
199
|
+
return response;
|
|
200
|
+
}
|
|
201
|
+
}
|