@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,576 @@
1
+ import type { RoleDefinition } from '../roles/types.ts';
2
+ import type { LLMMessage, LLMResponse, LLMStreamEvent, LLMToolCall, LLMTool, ContentBlock } from '../llm/provider.ts';
3
+ import { guardImageSize } from '../llm/provider.ts';
4
+ import { LLMManager } from '../llm/manager.ts';
5
+ import { AgentInstance } from './agent.ts';
6
+ import { AgentHierarchy } from './hierarchy.ts';
7
+ import { ToolRegistry, type ToolDefinition, isToolResult } from '../actions/tools/registry.ts';
8
+ import { toolDefToLLMTool } from '../actions/tools/builtin.ts';
9
+ import type { ActionCategory } from '../roles/authority.ts';
10
+ import type { AuthorityEngine } from '../authority/engine.ts';
11
+ import type { ApprovalManager, ApprovalRequest } from '../authority/approval.ts';
12
+ import type { AuditTrail } from '../authority/audit.ts';
13
+ import type { EmergencyController } from '../authority/emergency.ts';
14
+ import { getActionForTool } from '../authority/tool-action-map.ts';
15
+
16
+ const MAX_TOOL_ITERATIONS = 200;
17
+ const MAX_TOOL_RESULT_CHARS = 6000; // Cap individual tool results to control context size
18
+
19
+ export class AgentOrchestrator {
20
+ private hierarchy: AgentHierarchy;
21
+ private llmManager: LLMManager | null;
22
+ private toolRegistry: ToolRegistry | null;
23
+
24
+ // Authority engine components
25
+ private authorityEngine: AuthorityEngine | null = null;
26
+ private approvalManager: ApprovalManager | null = null;
27
+ private auditTrail: AuditTrail | null = null;
28
+ private emergencyController: EmergencyController | null = null;
29
+ private temporaryGrants: Map<string, ActionCategory[]> = new Map();
30
+ private onApprovalNeeded: ((request: ApprovalRequest) => void) | null = null;
31
+
32
+ constructor() {
33
+ this.hierarchy = new AgentHierarchy();
34
+ this.llmManager = null;
35
+ this.toolRegistry = null;
36
+ }
37
+
38
+ setLLMManager(llm: LLMManager): void {
39
+ this.llmManager = llm;
40
+ }
41
+
42
+ getLLMManager(): LLMManager | null {
43
+ return this.llmManager;
44
+ }
45
+
46
+ setToolRegistry(registry: ToolRegistry): void {
47
+ this.toolRegistry = registry;
48
+ }
49
+
50
+ getToolRegistry(): ToolRegistry | null {
51
+ return this.toolRegistry;
52
+ }
53
+
54
+ // --- Authority setters ---
55
+
56
+ setAuthorityEngine(engine: AuthorityEngine): void {
57
+ this.authorityEngine = engine;
58
+ }
59
+
60
+ setApprovalManager(manager: ApprovalManager): void {
61
+ this.approvalManager = manager;
62
+ }
63
+
64
+ setAuditTrail(trail: AuditTrail): void {
65
+ this.auditTrail = trail;
66
+ }
67
+
68
+ setEmergencyController(controller: EmergencyController): void {
69
+ this.emergencyController = controller;
70
+ }
71
+
72
+ setApprovalCallback(cb: (request: ApprovalRequest) => void): void {
73
+ this.onApprovalNeeded = cb;
74
+ }
75
+
76
+ /**
77
+ * Grant a temporary permission to a specific agent (for parent escalation).
78
+ */
79
+ grantTemporary(agentId: string, action: ActionCategory): void {
80
+ const existing = this.temporaryGrants.get(agentId) ?? [];
81
+ if (!existing.includes(action)) {
82
+ existing.push(action);
83
+ this.temporaryGrants.set(agentId, existing);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Revoke a temporary permission from an agent.
89
+ */
90
+ revokeTemporary(agentId: string, action: ActionCategory): void {
91
+ const existing = this.temporaryGrants.get(agentId);
92
+ if (existing) {
93
+ this.temporaryGrants.set(agentId, existing.filter(a => a !== action));
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Clear all temporary grants for an agent (called when task completes).
99
+ */
100
+ clearTemporaryGrants(agentId: string): void {
101
+ this.temporaryGrants.delete(agentId);
102
+ }
103
+
104
+ /**
105
+ * Create the primary agent from a role.
106
+ * No inline system prompt — the AgentService builds a rich dynamic prompt each turn.
107
+ */
108
+ createPrimary(role: RoleDefinition): AgentInstance {
109
+ const existing = this.hierarchy.getPrimary();
110
+ if (existing) {
111
+ throw new Error('Primary agent already exists. Terminate it first.');
112
+ }
113
+
114
+ const agent = new AgentInstance(role);
115
+ this.hierarchy.addAgent(agent);
116
+ return agent;
117
+ }
118
+
119
+ /**
120
+ * Spawn a sub-agent under a parent
121
+ */
122
+ spawnSubAgent(
123
+ parentId: string,
124
+ role: RoleDefinition,
125
+ opts?: { memory_scope?: string[] }
126
+ ): AgentInstance {
127
+ const parent = this.hierarchy.getAgent(parentId);
128
+ if (!parent) {
129
+ throw new Error(`Parent agent not found: ${parentId}`);
130
+ }
131
+
132
+ if (!parent.agent.authority.can_spawn_children) {
133
+ throw new Error('Parent agent does not have authority to spawn children');
134
+ }
135
+
136
+ // Create child agent with reduced authority
137
+ const childAuthority = {
138
+ max_authority_level: Math.min(
139
+ role.authority_level,
140
+ parent.agent.authority.max_authority_level - 1
141
+ ),
142
+ allowed_tools: role.tools.filter((tool) =>
143
+ parent.agent.authority.allowed_tools.includes(tool)
144
+ ),
145
+ denied_tools: parent.agent.authority.denied_tools,
146
+ max_token_budget: Math.floor(parent.agent.authority.max_token_budget / 2),
147
+ can_spawn_children: role.sub_roles.length > 0,
148
+ };
149
+
150
+ const agent = new AgentInstance(role, {
151
+ parent_id: parentId,
152
+ authority: childAuthority,
153
+ memory_scope: opts?.memory_scope ?? [],
154
+ });
155
+
156
+ this.hierarchy.addAgent(agent);
157
+
158
+ // Add system message with role context for sub-agents
159
+ agent.addMessage(
160
+ 'system',
161
+ `You are ${role.name}, spawned by ${parent.agent.role.name}. ${role.description}\n\nResponsibilities:\n${role.responsibilities.map((r) => `- ${r}`).join('\n')}\n\nYou report to: ${parent.agent.role.name}\n\nCommunication style: ${role.communication_style.tone} tone, ${role.communication_style.verbosity} verbosity, ${role.communication_style.formality} formality.`
162
+ );
163
+
164
+ return agent;
165
+ }
166
+
167
+ /**
168
+ * Terminate an agent and its children
169
+ */
170
+ terminateAgent(agentId: string): void {
171
+ const agent = this.hierarchy.getAgent(agentId);
172
+ if (!agent) {
173
+ throw new Error(`Agent not found: ${agentId}`);
174
+ }
175
+
176
+ // Recursively terminate children first
177
+ const children = this.hierarchy.getChildren(agentId);
178
+ for (const child of children) {
179
+ this.terminateAgent(child.id);
180
+ }
181
+
182
+ // Terminate this agent
183
+ agent.terminate();
184
+ this.hierarchy.removeAgent(agentId);
185
+ }
186
+
187
+ getPrimary(): AgentInstance | undefined {
188
+ return this.hierarchy.getPrimary();
189
+ }
190
+
191
+ getAgent(agentId: string): AgentInstance | undefined {
192
+ return this.hierarchy.getAgent(agentId);
193
+ }
194
+
195
+ getAllAgents(): AgentInstance[] {
196
+ return this.hierarchy.getAllAgents();
197
+ }
198
+
199
+ getHierarchy(): AgentHierarchy {
200
+ return this.hierarchy;
201
+ }
202
+
203
+ /**
204
+ * Process a user message through the primary agent (non-streaming).
205
+ * Includes the tool execution loop: LLM → tool_calls → execute → re-call → repeat.
206
+ */
207
+ async processMessage(systemPrompt: string, message: string): Promise<string> {
208
+ const primary = this.getPrimary();
209
+ if (!primary) {
210
+ throw new Error('No primary agent exists. Create one first.');
211
+ }
212
+
213
+ // Add user message to persistent history
214
+ primary.addMessage('user', message);
215
+
216
+ // If no LLM manager, return placeholder
217
+ if (!this.llmManager) {
218
+ const response = `[No LLM configured] Received: ${message}`;
219
+ primary.addMessage('assistant', response);
220
+ return response;
221
+ }
222
+
223
+ // Build local messages array for this turn (system + history)
224
+ const messages: LLMMessage[] = [
225
+ { role: 'system', content: systemPrompt },
226
+ ...primary.getMessages(),
227
+ ];
228
+
229
+ const tools = this.getLLMTools();
230
+ let finalText = '';
231
+
232
+ // Tool execution loop
233
+ for (let iteration = 0; iteration < MAX_TOOL_ITERATIONS; iteration++) {
234
+ const llmResponse: LLMResponse = await this.llmManager.chat(messages, { tools });
235
+
236
+ if (llmResponse.finish_reason === 'tool_use' && llmResponse.tool_calls.length > 0) {
237
+ // Add assistant message with tool calls to local messages
238
+ messages.push({
239
+ role: 'assistant',
240
+ content: llmResponse.content,
241
+ tool_calls: llmResponse.tool_calls,
242
+ });
243
+
244
+ // Execute each tool and add results
245
+ for (const tc of llmResponse.tool_calls) {
246
+ const result = await this.executeTool(tc);
247
+ messages.push({
248
+ role: 'tool',
249
+ content: result,
250
+ tool_call_id: tc.id,
251
+ });
252
+ const logStr = typeof result === 'string' ? result.slice(0, 100) : `[${result.length} content blocks]`;
253
+ console.log(`[Orchestrator] Tool ${tc.name} → ${logStr}...`);
254
+ }
255
+
256
+ // Continue loop to re-call LLM with tool results
257
+ continue;
258
+ }
259
+
260
+ // No tool calls — this is the final response
261
+ finalText = llmResponse.content;
262
+
263
+ // Warn on truncation
264
+ if (llmResponse.finish_reason === 'length') {
265
+ finalText += '\n\n[Response was truncated due to output token limits. If you asked for long content, ask to continue or use shorter chunks.]';
266
+ }
267
+
268
+ break;
269
+ }
270
+
271
+ // Add final response to persistent history
272
+ primary.addMessage('assistant', finalText);
273
+ return finalText;
274
+ }
275
+
276
+ /**
277
+ * Stream a message through the primary agent with tool execution loop.
278
+ * Yields text/tool_call events through all iterations.
279
+ * Only emits 'done' when the final response is complete.
280
+ */
281
+ async *streamMessage(systemPrompt: string, message: string): AsyncIterable<LLMStreamEvent> {
282
+ const primary = this.getPrimary();
283
+ if (!primary) {
284
+ throw new Error('No primary agent exists. Create one first.');
285
+ }
286
+
287
+ // Add user message to persistent history
288
+ primary.addMessage('user', message);
289
+
290
+ // If no LLM manager, yield placeholder
291
+ if (!this.llmManager) {
292
+ const response = `[No LLM configured] Received: ${message}`;
293
+ primary.addMessage('assistant', response);
294
+ yield { type: 'text', text: response };
295
+ yield {
296
+ type: 'done',
297
+ response: {
298
+ content: response,
299
+ tool_calls: [],
300
+ usage: { input_tokens: 0, output_tokens: 0 },
301
+ model: 'none',
302
+ finish_reason: 'stop',
303
+ },
304
+ };
305
+ return;
306
+ }
307
+
308
+ // Build local messages array for this turn
309
+ const messages: LLMMessage[] = [
310
+ { role: 'system', content: systemPrompt },
311
+ ...primary.getMessages(),
312
+ ];
313
+
314
+ const tools = this.getLLMTools();
315
+ const totalUsage = { input_tokens: 0, output_tokens: 0 };
316
+ let finalText = '';
317
+ let responseModel = 'unknown';
318
+
319
+ // Tool execution loop
320
+ for (let iteration = 0; iteration < MAX_TOOL_ITERATIONS; iteration++) {
321
+ let accumulatedText = '';
322
+ const toolCalls: LLMToolCall[] = [];
323
+ let doneResponse: LLMResponse | null = null;
324
+
325
+ // Stream from LLM
326
+ for await (const event of this.llmManager.stream(messages, { tools })) {
327
+ if (event.type === 'text') {
328
+ accumulatedText += event.text;
329
+ yield event; // Forward text chunks to client
330
+ } else if (event.type === 'tool_call') {
331
+ toolCalls.push(event.tool_call);
332
+ yield event; // Forward tool_call events to client
333
+ } else if (event.type === 'done') {
334
+ doneResponse = event.response;
335
+ totalUsage.input_tokens += event.response.usage.input_tokens;
336
+ totalUsage.output_tokens += event.response.usage.output_tokens;
337
+ responseModel = event.response.model;
338
+ // Don't yield done yet — may need more iterations
339
+ } else if (event.type === 'error') {
340
+ yield event;
341
+ return;
342
+ }
343
+ }
344
+
345
+ // Ensure doneResponse is never null (stream may end without 'done' event)
346
+ if (!doneResponse) {
347
+ doneResponse = {
348
+ content: accumulatedText,
349
+ tool_calls: toolCalls,
350
+ usage: { input_tokens: 0, output_tokens: 0 },
351
+ model: responseModel,
352
+ finish_reason: 'stop',
353
+ };
354
+ }
355
+
356
+ // No tool calls — this is the final response
357
+ if (toolCalls.length === 0) {
358
+ finalText += accumulatedText;
359
+
360
+ // Check if we stopped due to token limit (truncation)
361
+ const wasLength = doneResponse?.finish_reason === 'length';
362
+ if (wasLength && !finalText.includes('[SYSTEM WARNING')) {
363
+ const truncWarning = '\n\n[Response was truncated due to output token limits. If you asked for long content, ask to continue or use shorter chunks.]';
364
+ finalText += truncWarning;
365
+ yield { type: 'text', text: truncWarning };
366
+ }
367
+
368
+ yield {
369
+ type: 'done',
370
+ response: {
371
+ content: finalText,
372
+ tool_calls: [],
373
+ usage: totalUsage,
374
+ model: responseModel,
375
+ finish_reason: wasLength ? 'length' : 'stop',
376
+ },
377
+ };
378
+ // Add final response to persistent history (only user-facing text)
379
+ primary.addMessage('assistant', finalText);
380
+ return;
381
+ }
382
+
383
+ // Tool calls present — execute them
384
+ finalText += accumulatedText;
385
+
386
+ // Add assistant message with tool calls to local messages
387
+ messages.push({
388
+ role: 'assistant',
389
+ content: accumulatedText,
390
+ tool_calls: toolCalls,
391
+ });
392
+
393
+ // Execute each tool and add results
394
+ for (const tc of toolCalls) {
395
+ const result = await this.executeTool(tc);
396
+ messages.push({
397
+ role: 'tool',
398
+ content: result,
399
+ tool_call_id: tc.id,
400
+ });
401
+ const logStr = typeof result === 'string' ? result.slice(0, 100) : `[${result.length} content blocks]`;
402
+ console.log(`[Orchestrator] Tool ${tc.name} → ${logStr}...`);
403
+ }
404
+
405
+ // Continue loop — will stream next LLM response
406
+ }
407
+
408
+ // Max iterations reached
409
+ yield { type: 'text', text: '\n[Max tool iterations reached]' };
410
+ yield {
411
+ type: 'done',
412
+ response: {
413
+ content: finalText + '\n[Max tool iterations reached]',
414
+ tool_calls: [],
415
+ usage: totalUsage,
416
+ model: responseModel,
417
+ finish_reason: 'stop',
418
+ },
419
+ };
420
+ primary.addMessage('assistant', finalText);
421
+ }
422
+
423
+ /**
424
+ * Heartbeat: let the primary agent check for proactive actions.
425
+ */
426
+ async heartbeat(systemPrompt: string): Promise<string | null> {
427
+ const primary = this.getPrimary();
428
+ if (!primary || !this.llmManager) {
429
+ return null;
430
+ }
431
+
432
+ const messages: LLMMessage[] = [
433
+ { role: 'system', content: systemPrompt },
434
+ ...primary.getMessages(),
435
+ ];
436
+
437
+ const llmResponse: LLMResponse = await this.llmManager.chat(messages);
438
+
439
+ if (llmResponse.content && llmResponse.content.trim().length > 0) {
440
+ primary.addMessage('assistant', llmResponse.content);
441
+ return llmResponse.content;
442
+ }
443
+
444
+ return null;
445
+ }
446
+
447
+ // --- Private helpers ---
448
+
449
+ /**
450
+ * Get LLM-formatted tools from the ToolRegistry.
451
+ */
452
+ private getLLMTools(): LLMTool[] | undefined {
453
+ if (!this.toolRegistry || this.toolRegistry.count() === 0) {
454
+ return undefined;
455
+ }
456
+
457
+ return this.toolRegistry.list().map(toolDefToLLMTool);
458
+ }
459
+
460
+ /**
461
+ * Execute a single tool call via the ToolRegistry.
462
+ * Includes authority gate: checks emergency state, authority level, and governed categories.
463
+ * Returns a string for text-only results, or ContentBlock[] for multi-modal results (images).
464
+ */
465
+ private async executeTool(toolCall: LLMToolCall): Promise<string | ContentBlock[]> {
466
+ if (!this.toolRegistry) {
467
+ return `Error: No tool registry configured`;
468
+ }
469
+
470
+ // --- Authority Gate ---
471
+
472
+ // 1. Emergency check
473
+ if (this.emergencyController && !this.emergencyController.canExecute()) {
474
+ const state = this.emergencyController.getState();
475
+ return `[SYSTEM ${state.toUpperCase()}] All tool execution is currently suspended. The user has ${state} the system.`;
476
+ }
477
+
478
+ // 2. Authority check
479
+ const primary = this.getPrimary();
480
+ if (this.authorityEngine && primary) {
481
+ const tool = this.toolRegistry.get(toolCall.name);
482
+ const actionCategory = getActionForTool(toolCall.name, tool?.category ?? 'unknown');
483
+
484
+ const decision = this.authorityEngine.checkAuthority({
485
+ agentId: primary.id,
486
+ agentAuthorityLevel: primary.agent.authority.max_authority_level,
487
+ agentRoleId: primary.agent.role.id,
488
+ toolName: toolCall.name,
489
+ toolCategory: tool?.category ?? 'unknown',
490
+ actionCategory,
491
+ temporaryGrants: this.temporaryGrants,
492
+ });
493
+
494
+ // Determine decision type for audit
495
+ const decisionType = decision.allowed
496
+ ? (decision.requiresApproval ? 'approval_required' as const : 'allowed' as const)
497
+ : 'denied' as const;
498
+
499
+ // 3. Log to audit trail
500
+ this.auditTrail?.log({
501
+ agent_id: primary.id,
502
+ agent_name: primary.agent.role.name,
503
+ tool_name: toolCall.name,
504
+ action_category: actionCategory,
505
+ authority_decision: decisionType,
506
+ approval_id: null,
507
+ executed: decision.allowed && !decision.requiresApproval,
508
+ execution_time_ms: null,
509
+ });
510
+
511
+ // 4. Denied
512
+ if (!decision.allowed) {
513
+ return `[AUTHORITY DENIED] Cannot execute ${toolCall.name}: ${decision.reason}. Your authority level is insufficient for ${actionCategory} actions.`;
514
+ }
515
+
516
+ // 5. Requires approval
517
+ if (decision.requiresApproval && this.approvalManager) {
518
+ const urgency = this.determineUrgency(actionCategory);
519
+ const request = this.approvalManager.createRequest({
520
+ agentId: primary.id,
521
+ agentName: primary.agent.role.name,
522
+ toolName: toolCall.name,
523
+ toolArguments: toolCall.arguments,
524
+ actionCategory,
525
+ urgency,
526
+ reason: decision.reason,
527
+ context: `Agent attempted: ${toolCall.name}(${JSON.stringify(toolCall.arguments).slice(0, 200)})`,
528
+ });
529
+
530
+ // Emit approval request event
531
+ this.onApprovalNeeded?.(request);
532
+
533
+ return `[AWAITING_APPROVAL] Request #${request.id.slice(0, 8)} submitted. ` +
534
+ `Action: ${toolCall.name} (${actionCategory}). ` +
535
+ `Reason: ${decision.reason}. ` +
536
+ `The user will be notified and can approve or deny this action.`;
537
+ }
538
+ }
539
+
540
+ // --- Normal execution ---
541
+ try {
542
+ const startTime = Date.now();
543
+ const raw = await this.toolRegistry.execute(toolCall.name, toolCall.arguments);
544
+ const executionTimeMs = Date.now() - startTime;
545
+
546
+ // Update audit entry with execution time (for allowed actions)
547
+ // We already logged above; for simplicity we log execution separately if needed
548
+
549
+ // Multi-modal result (e.g. screenshot with image data)
550
+ if (isToolResult(raw)) {
551
+ return raw.content.map(guardImageSize);
552
+ }
553
+
554
+ // Plain text result
555
+ let result = typeof raw === 'string' ? raw : JSON.stringify(raw);
556
+
557
+ // Cap tool result size to control context growth
558
+ if (result.length > MAX_TOOL_RESULT_CHARS) {
559
+ result = result.slice(0, MAX_TOOL_RESULT_CHARS) + `\n... (truncated, was ${result.length} chars)`;
560
+ }
561
+
562
+ return result;
563
+ } catch (err) {
564
+ return `Error executing ${toolCall.name}: ${err instanceof Error ? err.message : String(err)}`;
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Determine urgency for an approval request based on action category.
570
+ */
571
+ private determineUrgency(actionCategory: ActionCategory): 'urgent' | 'normal' {
572
+ // Financial actions are always urgent
573
+ if (actionCategory === 'make_payment') return 'urgent';
574
+ return 'normal';
575
+ }
576
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Role Discovery — Specialist Role Loader
3
+ *
4
+ * Discovers specialist roles from a directory of YAML files.
5
+ * Returns a map of role definitions and a formatted list for system prompts.
6
+ */
7
+
8
+ import { join } from 'node:path';
9
+ import { loadRolesFromDir } from '../roles/loader.ts';
10
+ import type { RoleDefinition } from '../roles/types.ts';
11
+
12
+ /** Package root — resolves correctly whether running from repo or global install */
13
+ const PACKAGE_ROOT = join(import.meta.dir, '../..');
14
+
15
+ /**
16
+ * Discover specialist roles from a directory.
17
+ * Resolves relative paths against the package root (not CWD).
18
+ */
19
+ export function discoverSpecialists(dir: string): Map<string, RoleDefinition> {
20
+ const resolved = dir.startsWith('/') ? dir : join(PACKAGE_ROOT, dir);
21
+ return loadRolesFromDir(resolved);
22
+ }
23
+
24
+ /**
25
+ * Format the specialist map into a human-readable list for system prompts.
26
+ * The PA sees this and knows what specialists it can delegate to.
27
+ */
28
+ export function formatSpecialistList(specialists: Map<string, RoleDefinition>): string {
29
+ if (specialists.size === 0) {
30
+ return '';
31
+ }
32
+
33
+ const lines: string[] = ['## Available Specialists', ''];
34
+
35
+ for (const [id, role] of specialists) {
36
+ const desc = role.description.split('\n')
37
+ .map(l => l.trim())
38
+ .filter(l => l.length > 0)[0] ?? '';
39
+ const tools = role.tools.join(', ');
40
+ lines.push(`- **${role.name}** (\`${id}\`): ${desc} [tools: ${tools}]`);
41
+ }
42
+
43
+ lines.push('');
44
+ lines.push('## Delegation Strategy');
45
+ lines.push('');
46
+ lines.push('**Quick tasks** (research a question, write a draft, analyze data):');
47
+ lines.push('Use `delegate_task` — spawns a specialist, runs to completion, returns result. Blocks until done.');
48
+ lines.push('');
49
+ lines.push('**Complex / parallel work** (research + write, compare multiple topics, multi-step analysis):');
50
+ lines.push('Use `manage_agents`:');
51
+ lines.push('1. `spawn` the specialists you need');
52
+ lines.push('2. `assign` tasks to them (they run in the background in parallel)');
53
+ lines.push('3. `status` to check progress');
54
+ lines.push('4. `collect` results when done');
55
+ lines.push('5. `terminate` agents when no longer needed');
56
+ lines.push('');
57
+ lines.push('**When to delegate:** When a task falls into a specialist\'s domain and benefits from focused expertise.');
58
+ lines.push('For simple questions, handle them yourself. The user can explicitly ask to delegate.');
59
+
60
+ return lines.join('\n');
61
+ }