@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.
Files changed (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,600 @@
1
+ /**
2
+ * Agent Service — The Brain
3
+ *
4
+ * Owns the LLM manager, agent orchestrator, and personality state.
5
+ * Builds dynamic system prompts each turn with role context, personality,
6
+ * commitments, and observations.
7
+ */
8
+
9
+ import { join } from 'node:path';
10
+ import type { Service, ServiceStatus } from './services.ts';
11
+ import type { JarvisConfig } from '../config/types.ts';
12
+ import type { LLMStreamEvent } from '../llm/provider.ts';
13
+ import type { RoleDefinition } from '../roles/types.ts';
14
+ import type { PersonalityModel } from '../personality/model.ts';
15
+
16
+ import { LLMManager } from '../llm/manager.ts';
17
+ import { AnthropicProvider } from '../llm/anthropic.ts';
18
+ import { OpenAIProvider } from '../llm/openai.ts';
19
+ import { GeminiProvider } from '../llm/gemini.ts';
20
+ import { OllamaProvider } from '../llm/ollama.ts';
21
+ import { AgentOrchestrator } from '../agents/orchestrator.ts';
22
+ import { loadRole } from '../roles/loader.ts';
23
+ import { ToolRegistry } from '../actions/tools/registry.ts';
24
+ import { BUILTIN_TOOLS, browser } from '../actions/tools/builtin.ts';
25
+ import { createDelegateTool, type DelegateToolDeps } from '../actions/tools/delegate.ts';
26
+ import { createManageAgentsTool, type AgentToolDeps } from '../actions/tools/agents.ts';
27
+ import { contentPipelineTool } from '../actions/tools/content.ts';
28
+ import { commitmentsTool } from '../actions/tools/commitments.ts';
29
+ import { researchQueueTool } from '../actions/tools/research.ts';
30
+ import { AgentTaskManager } from '../agents/task-manager.ts';
31
+ import { discoverSpecialists, formatSpecialistList } from '../agents/role-discovery.ts';
32
+ import { buildSystemPrompt, type PromptContext } from '../roles/prompt-builder.ts';
33
+ import type { ProgressCallback } from '../agents/sub-agent-runner.ts';
34
+ import {
35
+ getPersonality,
36
+ savePersonality,
37
+ } from '../personality/model.ts';
38
+ import {
39
+ getChannelPersonality,
40
+ personalityToPrompt,
41
+ } from '../personality/adapter.ts';
42
+ import {
43
+ extractSignals,
44
+ applySignals,
45
+ recordInteraction,
46
+ } from '../personality/learner.ts';
47
+ import { getDueCommitments, getUpcoming } from '../vault/commitments.ts';
48
+ import { findContent } from '../vault/content-pipeline.ts';
49
+ import { getRecentObservations } from '../vault/observations.ts';
50
+ import { extractAndStore } from '../vault/extractor.ts';
51
+ import { getKnowledgeForMessage } from '../vault/retrieval.ts';
52
+ import type { ResearchQueue } from './research-queue.ts';
53
+ import type { IAgentService } from './agent-service-interface.ts';
54
+ import type { AuthorityEngine } from '../authority/engine.ts';
55
+ import { getSidecarManager } from '../actions/tools/sidecar-route.ts';
56
+
57
+ export class AgentService implements Service, IAgentService {
58
+ name = 'agent';
59
+ private _status: ServiceStatus = 'stopped';
60
+ private config: JarvisConfig;
61
+ private llmManager: LLMManager;
62
+ private orchestrator: AgentOrchestrator;
63
+ private role: RoleDefinition | null = null;
64
+ private personality: PersonalityModel | null = null;
65
+ private specialists: Map<string, RoleDefinition> = new Map();
66
+ private specialistListText: string = '';
67
+ private delegationProgressCallback: ProgressCallback | null = null;
68
+ private delegationCallback: ((specialistName: string, task: string) => void) | null = null;
69
+ private researchQueue: ResearchQueue | null = null;
70
+ private taskManager: AgentTaskManager | null = null;
71
+ private authorityEngine: AuthorityEngine | null = null;
72
+
73
+ constructor(config: JarvisConfig) {
74
+ this.config = config;
75
+ this.llmManager = new LLMManager();
76
+ this.orchestrator = new AgentOrchestrator();
77
+ }
78
+
79
+ /**
80
+ * Set callback for sub-agent progress events (delegation visibility).
81
+ * Typically wired to WebSocket broadcast by the daemon.
82
+ */
83
+ setDelegationProgressCallback(cb: ProgressCallback): void {
84
+ this.delegationProgressCallback = cb;
85
+ }
86
+
87
+ /**
88
+ * Set callback fired when the PA delegates a task to a specialist.
89
+ * Used by ws-service to update task board ownership in real time.
90
+ */
91
+ setDelegationCallback(cb: (specialistName: string, task: string) => void): void {
92
+ this.delegationCallback = cb;
93
+ }
94
+
95
+ /**
96
+ * Set the research queue for idle-time background research.
97
+ */
98
+ setResearchQueue(queue: ResearchQueue): void {
99
+ this.researchQueue = queue;
100
+ }
101
+
102
+ setAuthorityEngine(engine: AuthorityEngine): void {
103
+ this.authorityEngine = engine;
104
+ }
105
+
106
+
107
+ getOrchestrator(): AgentOrchestrator {
108
+ return this.orchestrator;
109
+ }
110
+
111
+ getLLMManager(): LLMManager {
112
+ return this.llmManager;
113
+ }
114
+
115
+ getTaskManager(): AgentTaskManager | null {
116
+ return this.taskManager;
117
+ }
118
+
119
+ async start(): Promise<void> {
120
+ this._status = 'starting';
121
+
122
+ try {
123
+ // 1. Create LLM providers from config
124
+ this.registerProviders();
125
+
126
+ // 2. Load role YAML
127
+ this.role = this.loadActiveRole();
128
+
129
+ // 3. Wire LLM manager to orchestrator
130
+ this.orchestrator.setLLMManager(this.llmManager);
131
+
132
+ // 4. Discover specialist roles
133
+ this.specialists = discoverSpecialists('roles/specialists');
134
+ if (this.specialists.size > 0) {
135
+ this.specialistListText = formatSpecialistList(this.specialists);
136
+ console.log(`[AgentService] Discovered ${this.specialists.size} specialists: ${Array.from(this.specialists.keys()).join(', ')}`);
137
+ }
138
+
139
+ // 5. Register tools (builtin + delegation)
140
+ const toolRegistry = new ToolRegistry();
141
+ for (const tool of BUILTIN_TOOLS) {
142
+ toolRegistry.register(tool);
143
+ }
144
+
145
+ // Register content pipeline tool
146
+ toolRegistry.register(contentPipelineTool);
147
+
148
+ // Register commitments tool
149
+ toolRegistry.register(commitmentsTool);
150
+
151
+ // Register research queue tool
152
+ toolRegistry.register(researchQueueTool);
153
+
154
+ // Register delegate_task tool if specialists are available
155
+ if (this.specialists.size > 0) {
156
+ const delegateDeps: DelegateToolDeps = {
157
+ orchestrator: this.orchestrator,
158
+ llmManager: this.llmManager,
159
+ specialists: this.specialists,
160
+ onProgress: (event) => {
161
+ if (this.delegationProgressCallback) {
162
+ this.delegationProgressCallback(event);
163
+ }
164
+ },
165
+ onDelegation: (specialistName, task) => {
166
+ if (this.delegationCallback) {
167
+ this.delegationCallback(specialistName, task);
168
+ }
169
+ },
170
+ };
171
+ const delegateTool = createDelegateTool(delegateDeps);
172
+ toolRegistry.register(delegateTool);
173
+ console.log('[AgentService] Registered delegate_task tool');
174
+
175
+ // Register manage_agents tool for persistent/async agents
176
+ this.taskManager = new AgentTaskManager();
177
+ const agentToolDeps: AgentToolDeps = {
178
+ orchestrator: this.orchestrator,
179
+ llmManager: this.llmManager,
180
+ specialists: this.specialists,
181
+ taskManager: this.taskManager,
182
+ onProgress: (event) => {
183
+ if (this.delegationProgressCallback) {
184
+ this.delegationProgressCallback(event);
185
+ }
186
+ },
187
+ };
188
+ const agentTool = createManageAgentsTool(agentToolDeps);
189
+ toolRegistry.register(agentTool);
190
+ console.log('[AgentService] Registered manage_agents tool');
191
+ }
192
+
193
+ this.orchestrator.setToolRegistry(toolRegistry);
194
+ console.log(`[AgentService] Registered ${toolRegistry.count()} tools total`);
195
+
196
+ // 6. Create primary agent
197
+ this.orchestrator.createPrimary(this.role);
198
+
199
+ // 7. Load personality
200
+ this.personality = getPersonality();
201
+
202
+ this._status = 'running';
203
+ console.log(`[AgentService] Started with role: ${this.role.name}`);
204
+ } catch (error) {
205
+ this._status = 'error';
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ async stop(): Promise<void> {
211
+ this._status = 'stopping';
212
+ const primary = this.orchestrator.getPrimary();
213
+ if (primary) {
214
+ this.orchestrator.terminateAgent(primary.id);
215
+ }
216
+
217
+ // Disconnect browser (stops auto-launched Chrome if any)
218
+ if (browser.connected) {
219
+ await browser.disconnect();
220
+ }
221
+
222
+ this._status = 'stopped';
223
+ console.log('[AgentService] Stopped');
224
+ }
225
+
226
+ status(): ServiceStatus {
227
+ return this._status;
228
+ }
229
+
230
+ /**
231
+ * Stream a message through the agent. Returns a stream and an onComplete callback.
232
+ */
233
+ streamMessage(text: string, channel: string = 'websocket'): {
234
+ stream: AsyncIterable<LLMStreamEvent>;
235
+ onComplete: (fullText: string) => Promise<void>;
236
+ } {
237
+ const systemPrompt = this.buildFullSystemPrompt(channel, text);
238
+
239
+ const stream = this.orchestrator.streamMessage(systemPrompt, text);
240
+
241
+ const onComplete = async (fullText: string): Promise<void> => {
242
+ // Note: orchestrator already adds assistant response to history
243
+ // Run extraction and learning in parallel, wait for both to settle
244
+ await Promise.allSettled([
245
+ this.extractKnowledge(text, fullText).catch((err) =>
246
+ console.error('[AgentService] Extraction error:', err instanceof Error ? err.message : err)
247
+ ),
248
+ this.learnFromInteraction(text, fullText, channel).catch((err) =>
249
+ console.error('[AgentService] Learning error:', err instanceof Error ? err.message : err)
250
+ ),
251
+ ]);
252
+ };
253
+
254
+ return { stream, onComplete };
255
+ }
256
+
257
+ /**
258
+ * Non-streaming message handler. Returns full response string.
259
+ */
260
+ async handleMessage(text: string, channel: string = 'websocket'): Promise<string> {
261
+ const systemPrompt = this.buildFullSystemPrompt(channel, text);
262
+
263
+ const response = await this.orchestrator.processMessage(systemPrompt, text);
264
+
265
+ // Run extraction and learning in parallel (non-blocking but tracked)
266
+ Promise.allSettled([
267
+ this.extractKnowledge(text, response).catch((err) =>
268
+ console.error('[AgentService] Extraction error:', err instanceof Error ? err.message : err)
269
+ ),
270
+ this.learnFromInteraction(text, response, channel).catch((err) =>
271
+ console.error('[AgentService] Learning error:', err instanceof Error ? err.message : err)
272
+ ),
273
+ ]);
274
+
275
+ return response;
276
+ }
277
+
278
+ /**
279
+ * Handle periodic heartbeat with full tool access.
280
+ * Accepts optional coalesced event summary to include in the prompt.
281
+ * Uses processMessage() so the agent can take action (browse, run commands, etc.).
282
+ */
283
+ async handleHeartbeat(coalescedEvents?: string): Promise<string | null> {
284
+ if (!this.role) return null;
285
+
286
+ const systemPrompt = this.buildHeartbeatPrompt(coalescedEvents);
287
+
288
+ // Build the heartbeat "user message" that triggers the agent
289
+ const parts: string[] = ['[HEARTBEAT] Periodic check-in. Review your responsibilities and take action.'];
290
+
291
+ if (coalescedEvents) {
292
+ parts.push('');
293
+ parts.push(coalescedEvents);
294
+ }
295
+
296
+ const heartbeatMessage = parts.join('\n');
297
+
298
+ try {
299
+ const response = await this.orchestrator.processMessage(systemPrompt, heartbeatMessage);
300
+ if (response && response.trim().length > 0) {
301
+ return response;
302
+ }
303
+ return null;
304
+ } catch (err) {
305
+ console.error('[AgentService] Heartbeat processing error:', err);
306
+ return null;
307
+ }
308
+ }
309
+
310
+ // --- Private methods ---
311
+
312
+ private registerProviders(): void {
313
+ const { llm } = this.config;
314
+ let hasProvider = false;
315
+
316
+ // Register Anthropic
317
+ if (llm.anthropic?.api_key) {
318
+ const provider = new AnthropicProvider(
319
+ llm.anthropic.api_key,
320
+ llm.anthropic.model
321
+ );
322
+ this.llmManager.registerProvider(provider);
323
+ hasProvider = true;
324
+ console.log('[AgentService] Registered Anthropic provider');
325
+ }
326
+
327
+ // Register OpenAI
328
+ if (llm.openai?.api_key) {
329
+ const provider = new OpenAIProvider(
330
+ llm.openai.api_key,
331
+ llm.openai.model
332
+ );
333
+ this.llmManager.registerProvider(provider);
334
+ hasProvider = true;
335
+ console.log('[AgentService] Registered OpenAI provider');
336
+ }
337
+
338
+ // Register Gemini
339
+ if (llm.gemini?.api_key) {
340
+ const provider = new GeminiProvider(
341
+ llm.gemini.api_key,
342
+ llm.gemini.model
343
+ );
344
+ this.llmManager.registerProvider(provider);
345
+ hasProvider = true;
346
+ console.log('[AgentService] Registered Gemini provider');
347
+ }
348
+
349
+ // Register Ollama (always available, no API key needed)
350
+ if (llm.ollama) {
351
+ const provider = new OllamaProvider(
352
+ llm.ollama.base_url,
353
+ llm.ollama.model
354
+ );
355
+ this.llmManager.registerProvider(provider);
356
+ hasProvider = true;
357
+ console.log('[AgentService] Registered Ollama provider');
358
+ }
359
+
360
+ if (!hasProvider) {
361
+ console.warn('[AgentService] No LLM providers configured. Responses will be placeholders.');
362
+ }
363
+
364
+ // Set primary and fallback chain
365
+ if (hasProvider) {
366
+ try {
367
+ this.llmManager.setPrimary(llm.primary);
368
+ } catch {
369
+ // Primary provider not available, first registered is already primary
370
+ }
371
+
372
+ // Set fallback chain (only for providers that were registered)
373
+ const registeredFallbacks = llm.fallback.filter(
374
+ (name) => this.llmManager.getProvider(name) !== undefined
375
+ );
376
+ if (registeredFallbacks.length > 0) {
377
+ this.llmManager.setFallbackChain(registeredFallbacks);
378
+ }
379
+ }
380
+ }
381
+
382
+ private loadActiveRole(): RoleDefinition {
383
+ const roleName = this.config.active_role;
384
+
385
+ // Try multiple locations for role YAML (package-root-relative for global install)
386
+ const pkgRoot = join(import.meta.dir, '../..');
387
+ const paths = [
388
+ join(pkgRoot, `roles/${roleName}.yaml`),
389
+ join(pkgRoot, `roles/${roleName}.yml`),
390
+ join(pkgRoot, `config/roles/${roleName}.yaml`),
391
+ join(pkgRoot, `config/roles/${roleName}.yml`),
392
+ // Also try CWD-relative for local dev
393
+ `roles/${roleName}.yaml`,
394
+ `roles/${roleName}.yml`,
395
+ ];
396
+
397
+ for (const rolePath of paths) {
398
+ try {
399
+ const role = loadRole(rolePath);
400
+ console.log(`[AgentService] Loaded role '${role.name}' from ${rolePath}`);
401
+ return role;
402
+ } catch {
403
+ // Try next path
404
+ }
405
+ }
406
+
407
+ // Fatal — cannot start without a role
408
+ throw new Error(
409
+ `[AgentService] Could not load role '${roleName}'. Searched: ${paths.join(', ')}`
410
+ );
411
+ }
412
+
413
+ private buildFullSystemPrompt(channel: string, userMessage?: string): string {
414
+ if (!this.role) return '';
415
+
416
+ // Build prompt context with live data + vault knowledge
417
+ const context = this.buildPromptContext(userMessage);
418
+
419
+ // Build base system prompt from role + context
420
+ const rolePrompt = buildSystemPrompt(this.role, context);
421
+
422
+ // Build personality prompt for this channel
423
+ const personality = this.personality ?? getPersonality();
424
+ const channelPersonality = getChannelPersonality(personality, channel);
425
+ const personalityPrompt = personalityToPrompt(channelPersonality);
426
+
427
+ return `${rolePrompt}\n\n${personalityPrompt}`;
428
+ }
429
+
430
+ private buildHeartbeatPrompt(coalescedEvents?: string): string {
431
+ if (!this.role) return '';
432
+
433
+ const context = this.buildPromptContext();
434
+ const rolePrompt = buildSystemPrompt(this.role, context);
435
+
436
+ const parts = [rolePrompt, '', '# Heartbeat Check', this.role.heartbeat_instructions];
437
+
438
+ if (coalescedEvents) {
439
+ parts.push('', '# Recent System Events', coalescedEvents);
440
+ }
441
+
442
+ // Inject commitment execution instructions
443
+ parts.push('', '# COMMITMENT EXECUTION');
444
+ parts.push('If any commitments are overdue or due soon, EXECUTE them now using your tools.');
445
+ parts.push('Do not just mention them — actually perform the work. Use browse, terminal, file operations as needed.');
446
+
447
+ // Inject background research instructions when idle
448
+ if (this.researchQueue && this.researchQueue.queuedCount() > 0) {
449
+ const next = this.researchQueue.getNext();
450
+ if (next) {
451
+ parts.push('', '# BACKGROUND RESEARCH');
452
+ parts.push(`You have a research topic queued: "${next.topic}"`);
453
+ parts.push(`Reason: ${next.reason}`);
454
+ parts.push(`Research ID: ${next.id}`);
455
+ parts.push('If nothing urgent needs your attention, research this topic now.');
456
+ parts.push('Use your browser and tools to gather information, then use the research_queue tool with action "complete" to save your findings.');
457
+ }
458
+ } else {
459
+ parts.push('', '# IDLE MODE');
460
+ parts.push('No research topics queued. If nothing urgent, you may:');
461
+ parts.push('- Check news or trends relevant to the user');
462
+ parts.push('- Review and organize pending tasks');
463
+ parts.push('- Or simply report "All clear" if nothing needs attention');
464
+ }
465
+
466
+ 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.');
467
+
468
+ return parts.join('\n');
469
+ }
470
+
471
+ private buildPromptContext(userMessage?: string): PromptContext {
472
+ // Check if any sidecars are enrolled (cheap DB query, controls tool guide content)
473
+ let hasSidecars = false;
474
+ try {
475
+ const mgr = getSidecarManager();
476
+ if (mgr) hasSidecars = mgr.listSidecars().length > 0;
477
+ } catch { /* ignore */ }
478
+
479
+ const context: PromptContext = {
480
+ currentTime: new Date().toISOString(),
481
+ availableSpecialists: this.specialistListText || undefined,
482
+ hasSidecars,
483
+ };
484
+
485
+ // Retrieve relevant knowledge from vault based on user message
486
+ if (userMessage) {
487
+ try {
488
+ const knowledge = getKnowledgeForMessage(userMessage);
489
+ if (knowledge) {
490
+ context.knowledgeContext = knowledge;
491
+ }
492
+ } catch (err) {
493
+ console.error('[AgentService] Error retrieving knowledge:', err);
494
+ }
495
+ }
496
+
497
+ // Get due commitments
498
+ try {
499
+ const due = getDueCommitments();
500
+ const upcoming = getUpcoming(5);
501
+ const allCommitments = [...due, ...upcoming];
502
+
503
+ if (allCommitments.length > 0) {
504
+ context.activeCommitments = allCommitments.map((c) => {
505
+ const dueStr = c.when_due
506
+ ? ` (due: ${new Date(c.when_due).toLocaleString()})`
507
+ : '';
508
+ return `[${c.priority}] ${c.what}${dueStr} — ${c.status}`;
509
+ });
510
+ }
511
+ } catch (err) {
512
+ console.error('[AgentService] Error loading commitments:', err);
513
+ }
514
+
515
+ // Get active content pipeline items (not published)
516
+ try {
517
+ const activeContent = findContent({}).filter(
518
+ (c) => c.stage !== 'published'
519
+ ).slice(0, 10);
520
+ if (activeContent.length > 0) {
521
+ context.contentPipeline = activeContent.map((c) => {
522
+ const tags = c.tags.length > 0 ? ` [${c.tags.join(', ')}]` : '';
523
+ return `"${c.title}" (${c.content_type}) — ${c.stage}${tags}`;
524
+ });
525
+ }
526
+ } catch (err) {
527
+ console.error('[AgentService] Error loading content pipeline:', err);
528
+ }
529
+
530
+ // Get recent observations
531
+ try {
532
+ const observations = getRecentObservations(undefined, 10);
533
+ if (observations.length > 0) {
534
+ context.recentObservations = observations.map((o) => {
535
+ const time = new Date(o.created_at).toLocaleTimeString();
536
+ return `[${time}] ${o.type}: ${JSON.stringify(o.data).slice(0, 200)}`;
537
+ });
538
+ }
539
+ } catch (err) {
540
+ console.error('[AgentService] Error loading observations:', err);
541
+ }
542
+
543
+ // Active goals context for the system prompt
544
+ try {
545
+ const { getActiveGoalsSummary } = require('../vault/retrieval.ts');
546
+ const goalsSummary = getActiveGoalsSummary();
547
+ if (goalsSummary) {
548
+ context.activeGoals = goalsSummary;
549
+ }
550
+ } catch {
551
+ // Goals module may not be available — ignore
552
+ }
553
+
554
+ // Authority rules for the system prompt
555
+ if (this.authorityEngine && this.role) {
556
+ try {
557
+ context.authorityRules = this.authorityEngine.describeRulesForAgent(
558
+ this.role.authority_level,
559
+ this.role.id
560
+ );
561
+ } catch (err) {
562
+ console.error('[AgentService] Error building authority rules:', err);
563
+ }
564
+ }
565
+
566
+ return context;
567
+ }
568
+
569
+ private async extractKnowledge(userMessage: string, assistantResponse: string): Promise<void> {
570
+ // Get the primary provider for extraction
571
+ const provider = this.llmManager.getProvider(this.config.llm.primary)
572
+ ?? this.llmManager.getProvider('anthropic')
573
+ ?? this.llmManager.getProvider('openai');
574
+
575
+ await extractAndStore(userMessage, assistantResponse, provider);
576
+ }
577
+
578
+ private async learnFromInteraction(
579
+ userMessage: string,
580
+ assistantResponse: string,
581
+ _channel: string
582
+ ): Promise<void> {
583
+ let personality = this.personality ?? getPersonality();
584
+
585
+ // Extract signals from the interaction
586
+ const signals = extractSignals(userMessage, assistantResponse);
587
+
588
+ // Apply signals if any
589
+ if (signals.length > 0) {
590
+ personality = applySignals(personality, signals);
591
+ }
592
+
593
+ // Record the interaction (increments message count, adjusts trust)
594
+ personality = recordInteraction(personality);
595
+
596
+ // Save updated personality
597
+ savePersonality(personality);
598
+ this.personality = personality;
599
+ }
600
+ }