@shykaruu/jarvis-brain 0.4.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 (330) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +428 -0
  3. package/bin/jarvis.ts +449 -0
  4. package/package.json +79 -0
  5. package/roles/activity-observer.yaml +60 -0
  6. package/roles/ceo-founder.yaml +144 -0
  7. package/roles/chief-of-staff.yaml +158 -0
  8. package/roles/dev-lead.yaml +182 -0
  9. package/roles/executive-assistant.yaml +77 -0
  10. package/roles/marketing-director.yaml +168 -0
  11. package/roles/personal-assistant.yaml +266 -0
  12. package/roles/research-specialist.yaml +60 -0
  13. package/roles/specialists/content-writer.yaml +53 -0
  14. package/roles/specialists/customer-support.yaml +57 -0
  15. package/roles/specialists/data-analyst.yaml +57 -0
  16. package/roles/specialists/financial-analyst.yaml +56 -0
  17. package/roles/specialists/hr-specialist.yaml +55 -0
  18. package/roles/specialists/legal-advisor.yaml +58 -0
  19. package/roles/specialists/marketing-strategist.yaml +56 -0
  20. package/roles/specialists/project-coordinator.yaml +55 -0
  21. package/roles/specialists/research-analyst.yaml +58 -0
  22. package/roles/specialists/software-engineer.yaml +57 -0
  23. package/roles/specialists/system-administrator.yaml +57 -0
  24. package/roles/system-admin.yaml +76 -0
  25. package/scripts/ensure-bun.cjs +16 -0
  26. package/src/actions/README.md +421 -0
  27. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  28. package/src/actions/app-control/desktop-controller.ts +438 -0
  29. package/src/actions/app-control/interface.ts +64 -0
  30. package/src/actions/app-control/linux.ts +273 -0
  31. package/src/actions/app-control/macos.ts +54 -0
  32. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  33. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  34. package/src/actions/app-control/windows.ts +44 -0
  35. package/src/actions/browser/cdp.ts +138 -0
  36. package/src/actions/browser/chrome-launcher.ts +261 -0
  37. package/src/actions/browser/session.ts +506 -0
  38. package/src/actions/browser/stealth.ts +49 -0
  39. package/src/actions/index.ts +20 -0
  40. package/src/actions/terminal/executor.ts +157 -0
  41. package/src/actions/terminal/wsl-bridge.ts +126 -0
  42. package/src/actions/test.ts +93 -0
  43. package/src/actions/tools/agents.ts +363 -0
  44. package/src/actions/tools/builtin.ts +950 -0
  45. package/src/actions/tools/commitments.ts +192 -0
  46. package/src/actions/tools/content.ts +217 -0
  47. package/src/actions/tools/delegate.ts +147 -0
  48. package/src/actions/tools/desktop.test.ts +55 -0
  49. package/src/actions/tools/desktop.ts +305 -0
  50. package/src/actions/tools/documents.ts +169 -0
  51. package/src/actions/tools/goals.ts +376 -0
  52. package/src/actions/tools/local-tools-guard.ts +31 -0
  53. package/src/actions/tools/registry.ts +173 -0
  54. package/src/actions/tools/research.ts +111 -0
  55. package/src/actions/tools/sidecar-list.ts +57 -0
  56. package/src/actions/tools/sidecar-route.ts +105 -0
  57. package/src/actions/tools/workflows.ts +216 -0
  58. package/src/agents/agent.ts +132 -0
  59. package/src/agents/delegation.ts +107 -0
  60. package/src/agents/hierarchy.ts +113 -0
  61. package/src/agents/index.ts +19 -0
  62. package/src/agents/messaging.ts +125 -0
  63. package/src/agents/orchestrator.ts +592 -0
  64. package/src/agents/role-discovery.ts +61 -0
  65. package/src/agents/sub-agent-runner.ts +309 -0
  66. package/src/agents/task-manager.ts +151 -0
  67. package/src/authority/approval-delivery.ts +59 -0
  68. package/src/authority/approval.ts +196 -0
  69. package/src/authority/audit.ts +158 -0
  70. package/src/authority/authority.test.ts +519 -0
  71. package/src/authority/deferred-executor.ts +103 -0
  72. package/src/authority/emergency.ts +66 -0
  73. package/src/authority/engine.ts +301 -0
  74. package/src/authority/index.ts +12 -0
  75. package/src/authority/learning.ts +111 -0
  76. package/src/authority/tool-action-map.ts +74 -0
  77. package/src/awareness/analytics.ts +466 -0
  78. package/src/awareness/awareness.test.ts +332 -0
  79. package/src/awareness/capture-engine.ts +305 -0
  80. package/src/awareness/context-graph.ts +130 -0
  81. package/src/awareness/context-tracker.ts +349 -0
  82. package/src/awareness/index.ts +25 -0
  83. package/src/awareness/intelligence.ts +321 -0
  84. package/src/awareness/ocr-engine.ts +88 -0
  85. package/src/awareness/service.ts +528 -0
  86. package/src/awareness/struggle-detector.ts +342 -0
  87. package/src/awareness/suggestion-engine.ts +476 -0
  88. package/src/awareness/types.ts +201 -0
  89. package/src/cli/autostart.ts +417 -0
  90. package/src/cli/deps.ts +449 -0
  91. package/src/cli/doctor.ts +238 -0
  92. package/src/cli/helpers.ts +401 -0
  93. package/src/cli/onboard.ts +827 -0
  94. package/src/cli/uninstall.test.ts +37 -0
  95. package/src/cli/uninstall.ts +202 -0
  96. package/src/comms/README.md +329 -0
  97. package/src/comms/auth-error.html +48 -0
  98. package/src/comms/channels/discord.ts +228 -0
  99. package/src/comms/channels/signal.ts +56 -0
  100. package/src/comms/channels/telegram.ts +316 -0
  101. package/src/comms/channels/whatsapp.ts +60 -0
  102. package/src/comms/channels.test.ts +173 -0
  103. package/src/comms/dashboard-auth.ts +75 -0
  104. package/src/comms/desktop-notify.ts +114 -0
  105. package/src/comms/example.ts +129 -0
  106. package/src/comms/index.ts +129 -0
  107. package/src/comms/streaming.ts +149 -0
  108. package/src/comms/voice.test.ts +504 -0
  109. package/src/comms/voice.ts +341 -0
  110. package/src/comms/websocket.test.ts +409 -0
  111. package/src/comms/websocket.ts +669 -0
  112. package/src/config/README.md +389 -0
  113. package/src/config/index.ts +6 -0
  114. package/src/config/loader.test.ts +183 -0
  115. package/src/config/loader.ts +148 -0
  116. package/src/config/types.ts +293 -0
  117. package/src/daemon/README.md +232 -0
  118. package/src/daemon/agent-service-interface.ts +9 -0
  119. package/src/daemon/agent-service.ts +667 -0
  120. package/src/daemon/api-routes.ts +3067 -0
  121. package/src/daemon/background-agent-service.ts +396 -0
  122. package/src/daemon/background-agent.test.ts +78 -0
  123. package/src/daemon/channel-service.ts +201 -0
  124. package/src/daemon/commitment-executor.ts +297 -0
  125. package/src/daemon/dashboard-auth.test.ts +170 -0
  126. package/src/daemon/event-classifier.ts +239 -0
  127. package/src/daemon/event-coalescer.ts +123 -0
  128. package/src/daemon/event-reactor.ts +214 -0
  129. package/src/daemon/flock.c +7 -0
  130. package/src/daemon/health.ts +220 -0
  131. package/src/daemon/index.ts +1070 -0
  132. package/src/daemon/llm-settings.test.ts +78 -0
  133. package/src/daemon/llm-settings.ts +450 -0
  134. package/src/daemon/observer-service.ts +150 -0
  135. package/src/daemon/pid.test.ts +283 -0
  136. package/src/daemon/pid.ts +224 -0
  137. package/src/daemon/research-queue.ts +155 -0
  138. package/src/daemon/services.ts +175 -0
  139. package/src/daemon/ws-service.ts +926 -0
  140. package/src/global.d.ts +4 -0
  141. package/src/goals/accountability.ts +240 -0
  142. package/src/goals/awareness-bridge.ts +185 -0
  143. package/src/goals/estimator.ts +185 -0
  144. package/src/goals/events.ts +28 -0
  145. package/src/goals/goals.test.ts +400 -0
  146. package/src/goals/integration.test.ts +329 -0
  147. package/src/goals/nl-builder.test.ts +220 -0
  148. package/src/goals/nl-builder.ts +256 -0
  149. package/src/goals/rhythm.test.ts +177 -0
  150. package/src/goals/rhythm.ts +275 -0
  151. package/src/goals/service.test.ts +135 -0
  152. package/src/goals/service.ts +407 -0
  153. package/src/goals/types.ts +106 -0
  154. package/src/goals/workflow-bridge.ts +96 -0
  155. package/src/integrations/google-api.ts +134 -0
  156. package/src/integrations/google-auth.ts +175 -0
  157. package/src/llm/README.md +291 -0
  158. package/src/llm/anthropic.ts +400 -0
  159. package/src/llm/gemini.ts +380 -0
  160. package/src/llm/groq.ts +406 -0
  161. package/src/llm/history.ts +147 -0
  162. package/src/llm/index.ts +21 -0
  163. package/src/llm/manager.ts +226 -0
  164. package/src/llm/ollama.ts +316 -0
  165. package/src/llm/openai.ts +411 -0
  166. package/src/llm/openrouter.ts +390 -0
  167. package/src/llm/provider.test.ts +487 -0
  168. package/src/llm/provider.ts +61 -0
  169. package/src/llm/test.ts +88 -0
  170. package/src/observers/README.md +278 -0
  171. package/src/observers/calendar.ts +113 -0
  172. package/src/observers/clipboard.ts +136 -0
  173. package/src/observers/email.ts +109 -0
  174. package/src/observers/example.ts +58 -0
  175. package/src/observers/file-watcher.ts +124 -0
  176. package/src/observers/index.ts +159 -0
  177. package/src/observers/notifications.ts +197 -0
  178. package/src/observers/observers.test.ts +203 -0
  179. package/src/observers/processes.ts +225 -0
  180. package/src/personality/README.md +61 -0
  181. package/src/personality/adapter.ts +196 -0
  182. package/src/personality/index.ts +20 -0
  183. package/src/personality/learner.ts +209 -0
  184. package/src/personality/model.ts +132 -0
  185. package/src/personality/personality.test.ts +236 -0
  186. package/src/roles/README.md +252 -0
  187. package/src/roles/authority.ts +120 -0
  188. package/src/roles/example-usage.ts +198 -0
  189. package/src/roles/index.ts +42 -0
  190. package/src/roles/loader.ts +143 -0
  191. package/src/roles/prompt-builder.ts +218 -0
  192. package/src/roles/test-multi.ts +102 -0
  193. package/src/roles/test-role.yaml +77 -0
  194. package/src/roles/test-utils.ts +93 -0
  195. package/src/roles/test.ts +106 -0
  196. package/src/roles/tool-guide.ts +195 -0
  197. package/src/roles/types.ts +36 -0
  198. package/src/roles/utils.ts +200 -0
  199. package/src/scripts/google-setup.ts +168 -0
  200. package/src/sidecar/connection.ts +179 -0
  201. package/src/sidecar/index.ts +6 -0
  202. package/src/sidecar/manager.ts +542 -0
  203. package/src/sidecar/protocol.ts +85 -0
  204. package/src/sidecar/rpc.ts +161 -0
  205. package/src/sidecar/scheduler.ts +136 -0
  206. package/src/sidecar/types.ts +112 -0
  207. package/src/sidecar/validator.ts +144 -0
  208. package/src/sites/builder-tools.ts +215 -0
  209. package/src/sites/dev-server-manager.ts +286 -0
  210. package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
  211. package/src/sites/fixtures/security-test-site/Makefile +15 -0
  212. package/src/sites/fixtures/security-test-site/README.md +18 -0
  213. package/src/sites/fixtures/security-test-site/index.html +12 -0
  214. package/src/sites/fixtures/security-test-site/index.ts +16 -0
  215. package/src/sites/fixtures/security-test-site/package.json +13 -0
  216. package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
  217. package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
  218. package/src/sites/git-manager.ts +240 -0
  219. package/src/sites/github-manager.ts +355 -0
  220. package/src/sites/index.ts +25 -0
  221. package/src/sites/project-manager.ts +389 -0
  222. package/src/sites/proxy.ts +133 -0
  223. package/src/sites/service.ts +136 -0
  224. package/src/sites/templates.ts +169 -0
  225. package/src/sites/types.ts +89 -0
  226. package/src/user/profile-followup.test.ts +84 -0
  227. package/src/user/profile-followup.ts +185 -0
  228. package/src/user/profile.ts +224 -0
  229. package/src/vault/README.md +110 -0
  230. package/src/vault/awareness.ts +341 -0
  231. package/src/vault/commitments.ts +299 -0
  232. package/src/vault/content-pipeline.ts +270 -0
  233. package/src/vault/conversations.ts +173 -0
  234. package/src/vault/dashboard-sessions.ts +44 -0
  235. package/src/vault/documents.ts +130 -0
  236. package/src/vault/entities.ts +185 -0
  237. package/src/vault/extractor.test.ts +356 -0
  238. package/src/vault/extractor.ts +345 -0
  239. package/src/vault/facts.ts +190 -0
  240. package/src/vault/goals.ts +477 -0
  241. package/src/vault/index.ts +87 -0
  242. package/src/vault/keychain.ts +99 -0
  243. package/src/vault/observations.ts +115 -0
  244. package/src/vault/relationships.ts +178 -0
  245. package/src/vault/retrieval.test.ts +139 -0
  246. package/src/vault/retrieval.ts +258 -0
  247. package/src/vault/schema.ts +709 -0
  248. package/src/vault/settings.ts +38 -0
  249. package/src/vault/user-profile.test.ts +113 -0
  250. package/src/vault/user-profile.ts +176 -0
  251. package/src/vault/vectors.ts +92 -0
  252. package/src/vault/webapp-template-seeds.ts +116 -0
  253. package/src/vault/webapp-templates.ts +244 -0
  254. package/src/vault/workflows.ts +403 -0
  255. package/src/workflows/auto-suggest.ts +290 -0
  256. package/src/workflows/engine.ts +366 -0
  257. package/src/workflows/events.ts +24 -0
  258. package/src/workflows/executor.ts +207 -0
  259. package/src/workflows/nl-builder.ts +198 -0
  260. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  261. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  262. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  263. package/src/workflows/nodes/actions/discord.ts +77 -0
  264. package/src/workflows/nodes/actions/file-write.ts +73 -0
  265. package/src/workflows/nodes/actions/gmail.ts +69 -0
  266. package/src/workflows/nodes/actions/http-request.ts +117 -0
  267. package/src/workflows/nodes/actions/notification.ts +85 -0
  268. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  269. package/src/workflows/nodes/actions/send-message.ts +82 -0
  270. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  271. package/src/workflows/nodes/actions/telegram.ts +60 -0
  272. package/src/workflows/nodes/builtin.ts +119 -0
  273. package/src/workflows/nodes/error/error-handler.ts +37 -0
  274. package/src/workflows/nodes/error/fallback.ts +47 -0
  275. package/src/workflows/nodes/error/retry.ts +82 -0
  276. package/src/workflows/nodes/logic/delay.ts +42 -0
  277. package/src/workflows/nodes/logic/if-else.ts +41 -0
  278. package/src/workflows/nodes/logic/loop.ts +90 -0
  279. package/src/workflows/nodes/logic/merge.ts +38 -0
  280. package/src/workflows/nodes/logic/race.ts +40 -0
  281. package/src/workflows/nodes/logic/switch.ts +59 -0
  282. package/src/workflows/nodes/logic/template-render.ts +53 -0
  283. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  284. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  285. package/src/workflows/nodes/registry.ts +99 -0
  286. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  287. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  288. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  289. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  290. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  291. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  292. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  293. package/src/workflows/nodes/triggers/cron.ts +40 -0
  294. package/src/workflows/nodes/triggers/email.ts +40 -0
  295. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  296. package/src/workflows/nodes/triggers/git.ts +46 -0
  297. package/src/workflows/nodes/triggers/manual.ts +23 -0
  298. package/src/workflows/nodes/triggers/poll.ts +81 -0
  299. package/src/workflows/nodes/triggers/process.ts +44 -0
  300. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  301. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  302. package/src/workflows/safe-eval.ts +139 -0
  303. package/src/workflows/template.ts +118 -0
  304. package/src/workflows/triggers/cron.ts +311 -0
  305. package/src/workflows/triggers/manager.ts +285 -0
  306. package/src/workflows/triggers/observer-bridge.ts +172 -0
  307. package/src/workflows/triggers/poller.ts +201 -0
  308. package/src/workflows/triggers/screen-condition.ts +218 -0
  309. package/src/workflows/triggers/triggers.test.ts +740 -0
  310. package/src/workflows/triggers/webhook.ts +191 -0
  311. package/src/workflows/types.ts +133 -0
  312. package/src/workflows/variables.ts +72 -0
  313. package/src/workflows/workflows.test.ts +383 -0
  314. package/src/workflows/yaml.ts +104 -0
  315. package/ui/dist/index-3gr23jt9.js +112614 -0
  316. package/ui/dist/index-9vmj8127.css +14239 -0
  317. package/ui/dist/index-hy9pc1gm.js +112873 -0
  318. package/ui/dist/index-j2ep5d1w.js +112374 -0
  319. package/ui/dist/index-jt00vjqs.js +112858 -0
  320. package/ui/dist/index-k9ymx5qb.js +112374 -0
  321. package/ui/dist/index.html +16 -0
  322. package/ui/public/audio/pcm-capture-processor.js +11 -0
  323. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  324. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  325. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  326. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  327. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  328. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  329. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  330. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,226 @@
1
+ import type {
2
+ LLMProvider,
3
+ LLMMessage,
4
+ LLMOptions,
5
+ LLMResponse,
6
+ LLMStreamEvent,
7
+ } from './provider.ts';
8
+
9
+ export class LLMManager {
10
+ private providers: Map<string, LLMProvider> = new Map();
11
+ private primaryProvider = '';
12
+ private fallbackChain: string[] = [];
13
+ private static readonly MAX_RETRIES_PER_PROVIDER = 3;
14
+ private static readonly REQUEST_TIMEOUT_MS = 90000; // 90 second timeout for LLM calls
15
+ private static readonly isDebugging = process.env.JARVIS_LOG_LEVEL === 'debug' || process.env.DEBUG_LLM === 'true';
16
+
17
+ constructor() {}
18
+
19
+ registerProvider(provider: LLMProvider): void {
20
+ this.providers.set(provider.name, provider);
21
+
22
+ // Set as primary if it's the first provider
23
+ if (!this.primaryProvider) {
24
+ this.primaryProvider = provider.name;
25
+ }
26
+ }
27
+
28
+ setPrimary(name: string): void {
29
+ if (!this.providers.has(name)) {
30
+ throw new Error(`Provider '${name}' not registered`);
31
+ }
32
+ this.primaryProvider = name;
33
+ }
34
+
35
+ setFallbackChain(names: string[]): void {
36
+ for (const name of names) {
37
+ if (!this.providers.has(name)) {
38
+ throw new Error(`Provider '${name}' not registered`);
39
+ }
40
+ }
41
+ this.fallbackChain = names;
42
+ }
43
+
44
+ getProvider(name: string): LLMProvider | undefined {
45
+ return this.providers.get(name);
46
+ }
47
+
48
+ getPrimary(): string {
49
+ return this.primaryProvider;
50
+ }
51
+
52
+ getFallbackChain(): string[] {
53
+ return [...this.fallbackChain];
54
+ }
55
+
56
+ getProviderNames(): string[] {
57
+ return [...this.providers.keys()];
58
+ }
59
+
60
+ /**
61
+ * Atomically replace all providers. Safe for in-flight requests because
62
+ * JS is single-threaded and the map assignment is atomic.
63
+ */
64
+ replaceProviders(providers: LLMProvider[], primary: string, fallback: string[]): void {
65
+ const newMap = new Map<string, LLMProvider>();
66
+ for (const p of providers) {
67
+ newMap.set(p.name, p);
68
+ }
69
+ this.providers = newMap;
70
+ this.primaryProvider = newMap.has(primary) ? primary : (providers[0]?.name ?? '');
71
+ this.fallbackChain = fallback.filter(n => newMap.has(n));
72
+ }
73
+
74
+ /**
75
+ * Add request timeout wrapper for network resilience
76
+ */
77
+ private async withTimeout<T>(promise: Promise<T>, provider: string): Promise<T> {
78
+ return Promise.race([
79
+ promise,
80
+ new Promise<T>((_, reject) =>
81
+ setTimeout(
82
+ () => reject(new Error(`LLM request to ${provider} timed out after ${LLMManager.REQUEST_TIMEOUT_MS}ms`)),
83
+ LLMManager.REQUEST_TIMEOUT_MS
84
+ )
85
+ )
86
+ ]);
87
+ }
88
+
89
+ /**
90
+ * Classify error for better retry logic
91
+ */
92
+ private shouldRetry(error: unknown): boolean {
93
+ if (!(error instanceof Error)) return false;
94
+
95
+ const msg = error.message.toLowerCase();
96
+ // Retry on network/timeout errors, not on auth/validation errors
97
+ return msg.includes('timeout') ||
98
+ msg.includes('econnrefused') ||
99
+ msg.includes('enotfound') ||
100
+ msg.includes('network') ||
101
+ msg.includes('temporarily unavailable') ||
102
+ msg.includes('429') || // rate limit
103
+ msg.includes('503'); // service unavailable
104
+ }
105
+
106
+ /**
107
+ * Temporarily override the primary provider for a single call.
108
+ * Used for per-message LLM selection from chat dashboard.
109
+ */
110
+ async chatWithOverride(
111
+ messages: LLMMessage[],
112
+ overridePrimary: string | null,
113
+ options?: LLMOptions
114
+ ): Promise<LLMResponse> {
115
+ const providerName = overridePrimary && this.providers.has(overridePrimary) ? overridePrimary : this.primaryProvider;
116
+ const provider = this.providers.get(providerName);
117
+ if (!provider) {
118
+ throw new Error(`Provider '${providerName}' not registered`);
119
+ }
120
+
121
+ const errors: string[] = [];
122
+ for (let attempt = 1; attempt <= LLMManager.MAX_RETRIES_PER_PROVIDER; attempt++) {
123
+ try {
124
+ const result = await this.withTimeout(provider.chat(messages, options), providerName);
125
+ if (LLMManager.isDebugging && attempt > 1) {
126
+ console.log(`[DEBUG] LLM ${providerName} succeeded on retry attempt ${attempt}`);
127
+ }
128
+ return result;
129
+ } catch (err) {
130
+ const errorMsg = err instanceof Error ? err.message : String(err);
131
+ errors.push(`attempt ${attempt}: ${errorMsg}`);
132
+
133
+ const shouldRetry = this.shouldRetry(err);
134
+ console.error(
135
+ `[LLM] Provider ${providerName} failed (attempt ${attempt}/${LLMManager.MAX_RETRIES_PER_PROVIDER})${!shouldRetry ? ' [no retry]' : ''}: ${errorMsg}`
136
+ );
137
+
138
+ // Don't retry on fatal errors (auth, validation)
139
+ if (!shouldRetry && attempt > 1) break;
140
+ }
141
+ }
142
+
143
+ throw new Error(
144
+ `Provider '${providerName}' failed after ${LLMManager.MAX_RETRIES_PER_PROVIDER} attempts:\n${errors.map(e => ` ${e}`).join('\n')}`
145
+ );
146
+ }
147
+
148
+ async chat(messages: LLMMessage[], options?: LLMOptions): Promise<LLMResponse> {
149
+ const providerName = this.primaryProvider;
150
+ const provider = this.providers.get(providerName);
151
+ if (!provider) {
152
+ throw new Error(`Provider '${providerName}' not registered`);
153
+ }
154
+
155
+ const errors: string[] = [];
156
+ for (let attempt = 1; attempt <= LLMManager.MAX_RETRIES_PER_PROVIDER; attempt++) {
157
+ try {
158
+ const result = await this.withTimeout(provider.chat(messages, options), providerName);
159
+ if (LLMManager.isDebugging && attempt > 1) {
160
+ console.log(`[DEBUG] LLM ${providerName} succeeded on retry attempt ${attempt}`);
161
+ }
162
+ return result;
163
+ } catch (err) {
164
+ const errorMsg = err instanceof Error ? err.message : String(err);
165
+ errors.push(`attempt ${attempt}: ${errorMsg}`);
166
+
167
+ const shouldRetry = this.shouldRetry(err);
168
+ console.error(
169
+ `[LLM] Provider ${providerName} failed (attempt ${attempt}/${LLMManager.MAX_RETRIES_PER_PROVIDER})${!shouldRetry ? ' [no retry]' : ''}: ${errorMsg}`
170
+ );
171
+
172
+ if (!shouldRetry && attempt > 1) break;
173
+ }
174
+ }
175
+
176
+ throw new Error(
177
+ `Provider '${providerName}' failed after ${LLMManager.MAX_RETRIES_PER_PROVIDER} attempts:\n${errors.map(e => ` ${e}`).join('\n')}`
178
+ );
179
+ }
180
+
181
+ async *stream(messages: LLMMessage[], options?: LLMOptions): AsyncIterable<LLMStreamEvent> {
182
+ const providerName = this.primaryProvider;
183
+ const provider = this.providers.get(providerName);
184
+ if (!provider) {
185
+ yield { type: 'error', error: `Provider '${providerName}' not registered` };
186
+ return;
187
+ }
188
+
189
+ const errors: string[] = [];
190
+ for (let attempt = 1; attempt <= LLMManager.MAX_RETRIES_PER_PROVIDER; attempt++) {
191
+ try {
192
+ let hasError = false;
193
+ for await (const event of provider.stream(messages, options)) {
194
+ if (event.type === 'error') {
195
+ hasError = true;
196
+ errors.push(`attempt ${attempt}: ${event.error}`);
197
+ console.error(
198
+ `[LLM] Provider ${providerName} stream error (attempt ${attempt}/${LLMManager.MAX_RETRIES_PER_PROVIDER}): ${event.error}`
199
+ );
200
+ break;
201
+ }
202
+ yield event;
203
+ }
204
+
205
+ if (!hasError) {
206
+ return;
207
+ }
208
+ } catch (err) {
209
+ const errorMsg = err instanceof Error ? err.message : String(err);
210
+ errors.push(`attempt ${attempt}: ${errorMsg}`);
211
+
212
+ const shouldRetry = this.shouldRetry(err);
213
+ console.error(
214
+ `[LLM] Provider ${providerName} stream failed (attempt ${attempt}/${LLMManager.MAX_RETRIES_PER_PROVIDER})${!shouldRetry ? ' [no retry]' : ''}: ${errorMsg}`
215
+ );
216
+
217
+ if (!shouldRetry && attempt > 1) break;
218
+ }
219
+ }
220
+
221
+ yield {
222
+ type: 'error',
223
+ error: `Provider '${providerName}' failed after ${LLMManager.MAX_RETRIES_PER_PROVIDER} attempts:\n${errors.map(e => ` ${e}`).join('\n')}`,
224
+ };
225
+ }
226
+ }
@@ -0,0 +1,316 @@
1
+ import type {
2
+ LLMProvider,
3
+ LLMMessage,
4
+ LLMOptions,
5
+ LLMResponse,
6
+ LLMStreamEvent,
7
+ LLMTool,
8
+ LLMToolCall,
9
+ } from './provider.ts';
10
+ import { compactHistory, calculateHistoryBudget } from './history.ts';
11
+
12
+ type OllamaMessage = {
13
+ role: 'system' | 'user' | 'assistant';
14
+ content: string;
15
+ images?: string[];
16
+ };
17
+
18
+ type OllamaToolDef = {
19
+ type: 'function';
20
+ function: {
21
+ name: string;
22
+ description: string;
23
+ parameters: Record<string, unknown>;
24
+ };
25
+ };
26
+
27
+ type OllamaResponse = {
28
+ model: string;
29
+ created_at: string;
30
+ message: {
31
+ role: 'assistant';
32
+ content: string;
33
+ tool_calls?: Array<{
34
+ function: {
35
+ name: string;
36
+ arguments: Record<string, unknown>;
37
+ };
38
+ }>;
39
+ };
40
+ done: boolean;
41
+ total_duration?: number;
42
+ load_duration?: number;
43
+ prompt_eval_count?: number;
44
+ eval_count?: number;
45
+ };
46
+
47
+ type OllamaStreamChunk = {
48
+ model: string;
49
+ created_at: string;
50
+ message?: {
51
+ role: 'assistant';
52
+ content: string;
53
+ tool_calls?: Array<{
54
+ function: {
55
+ name: string;
56
+ arguments: Record<string, unknown>;
57
+ };
58
+ }>;
59
+ };
60
+ done: boolean;
61
+ total_duration?: number;
62
+ prompt_eval_count?: number;
63
+ eval_count?: number;
64
+ };
65
+
66
+ type OllamaModelInfo = {
67
+ name: string;
68
+ model: string;
69
+ modified_at: string;
70
+ size: number;
71
+ digest: string;
72
+ };
73
+
74
+ export class OllamaProvider implements LLMProvider {
75
+ name = 'ollama';
76
+ private baseUrl: string;
77
+ private defaultModel: string;
78
+
79
+ constructor(baseUrl = 'http://localhost:11434', defaultModel = 'llama3') {
80
+ this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
81
+ this.defaultModel = defaultModel;
82
+ }
83
+
84
+ async chat(messages: LLMMessage[], options: LLMOptions = {}): Promise<LLMResponse> {
85
+ const { model = this.defaultModel, temperature, tools, tool_choice } = options;
86
+
87
+ // Compact history for Ollama's context limits
88
+ const budget = calculateHistoryBudget(32000);
89
+ const compactedMessages = compactHistory(messages, budget);
90
+
91
+ const body: Record<string, unknown> = {
92
+ model,
93
+ messages: this.convertMessages(compactedMessages),
94
+ stream: false,
95
+ };
96
+
97
+ if (temperature !== undefined) {
98
+ body.options = { temperature };
99
+ }
100
+
101
+ if (tools && tools.length > 0) {
102
+ body.tools = this.convertTools(tools);
103
+ }
104
+
105
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
106
+ method: 'POST',
107
+ headers: {
108
+ 'Content-Type': 'application/json',
109
+ },
110
+ body: JSON.stringify(body),
111
+ });
112
+
113
+ if (!response.ok) {
114
+ const errorText = await response.text();
115
+ throw new Error(`Ollama API error (${response.status}): ${errorText}`);
116
+ }
117
+
118
+ const data = await response.json() as OllamaResponse;
119
+ return this.convertResponse(data);
120
+ }
121
+
122
+ async *stream(messages: LLMMessage[], options: LLMOptions = {}): AsyncIterable<LLMStreamEvent> {
123
+ const { model = this.defaultModel, temperature, tools, tool_choice } = options;
124
+
125
+ // Compact history for Ollama's context limits
126
+ const budget = calculateHistoryBudget(32000);
127
+ const compactedMessages = compactHistory(messages, budget);
128
+
129
+ const body: Record<string, unknown> = {
130
+ model,
131
+ messages: this.convertMessages(compactedMessages),
132
+ stream: true,
133
+ };
134
+
135
+ if (temperature !== undefined) {
136
+ body.options = { temperature };
137
+ }
138
+
139
+ if (tools && tools.length > 0) {
140
+ body.tools = this.convertTools(tools);
141
+ }
142
+
143
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ },
148
+ body: JSON.stringify(body),
149
+ });
150
+
151
+ if (!response.ok) {
152
+ const errorText = await response.text();
153
+ yield { type: 'error', error: `Ollama API error (${response.status}): ${errorText}` };
154
+ return;
155
+ }
156
+
157
+ if (!response.body) {
158
+ yield { type: 'error', error: 'No response body' };
159
+ return;
160
+ }
161
+
162
+ let accumulatedText = '';
163
+ const toolCalls: LLMToolCall[] = [];
164
+ let responseModel = model;
165
+ let inputTokens = 0;
166
+ let outputTokens = 0;
167
+
168
+ try {
169
+ const reader = response.body.getReader();
170
+ const decoder = new TextDecoder();
171
+ let buffer = '';
172
+
173
+ while (true) {
174
+ const { done, value } = await reader.read();
175
+ if (done) break;
176
+
177
+ buffer += decoder.decode(value, { stream: true });
178
+ const lines = buffer.split('\n');
179
+ buffer = lines.pop() || '';
180
+
181
+ for (const line of lines) {
182
+ if (!line.trim()) continue;
183
+
184
+ try {
185
+ const chunk = JSON.parse(line) as OllamaStreamChunk;
186
+ responseModel = chunk.model;
187
+
188
+ if (chunk.message?.content) {
189
+ accumulatedText += chunk.message.content;
190
+ yield { type: 'text', text: chunk.message.content };
191
+ }
192
+
193
+ if (chunk.message?.tool_calls) {
194
+ for (const toolCall of chunk.message.tool_calls) {
195
+ const id = `ollama_${Date.now()}_${Math.random().toString(36).substring(7)}`;
196
+ const call: LLMToolCall = {
197
+ id,
198
+ name: toolCall.function.name,
199
+ arguments: toolCall.function.arguments,
200
+ };
201
+ toolCalls.push(call);
202
+ yield { type: 'tool_call', tool_call: call };
203
+ }
204
+ }
205
+
206
+ if (chunk.done) {
207
+ inputTokens = chunk.prompt_eval_count || 0;
208
+ outputTokens = chunk.eval_count || 0;
209
+
210
+ yield {
211
+ type: 'done',
212
+ response: {
213
+ content: accumulatedText,
214
+ tool_calls: toolCalls,
215
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens },
216
+ model: responseModel,
217
+ finish_reason: toolCalls.length > 0 ? 'tool_use' : 'stop',
218
+ },
219
+ };
220
+ }
221
+ } catch (err) {
222
+ console.error('Failed to parse Ollama chunk:', err);
223
+ }
224
+ }
225
+ }
226
+ } catch (err) {
227
+ yield { type: 'error', error: `Stream error: ${err}` };
228
+ }
229
+ }
230
+
231
+ async listModels(): Promise<string[]> {
232
+ try {
233
+ const response = await fetch(`${this.baseUrl}/api/tags`);
234
+
235
+ if (!response.ok) {
236
+ throw new Error(`Failed to list models: ${response.status}`);
237
+ }
238
+
239
+ const data = await response.json() as { models: OllamaModelInfo[] };
240
+ return data.models.map(m => m.name).sort();
241
+ } catch (err) {
242
+ // Fallback to common models if API call fails
243
+ return ['llama3', 'llama2', 'mistral', 'mixtral', 'codellama'];
244
+ }
245
+ }
246
+
247
+ private convertMessages(messages: LLMMessage[]): OllamaMessage[] {
248
+ return messages.map(m => {
249
+ if (typeof m.content === 'string') {
250
+ return {
251
+ role: m.role as 'system' | 'user' | 'assistant',
252
+ content: m.content,
253
+ };
254
+ }
255
+
256
+ // ContentBlock[] — extract text and images separately
257
+ let text = '';
258
+ const images: string[] = [];
259
+
260
+ for (const block of m.content) {
261
+ if (block.type === 'text') {
262
+ text += (text ? '\n' : '') + block.text;
263
+ } else if (block.type === 'image') {
264
+ images.push(block.source.data);
265
+ }
266
+ }
267
+
268
+ const msg: OllamaMessage = {
269
+ role: m.role as 'system' | 'user' | 'assistant',
270
+ content: text,
271
+ };
272
+ if (images.length > 0) {
273
+ msg.images = images;
274
+ }
275
+ return msg;
276
+ });
277
+ }
278
+
279
+ private convertTools(tools: LLMTool[]): OllamaToolDef[] {
280
+ return tools.map(tool => ({
281
+ type: 'function',
282
+ function: {
283
+ name: tool.name,
284
+ description: tool.description,
285
+ parameters: tool.parameters,
286
+ },
287
+ }));
288
+ }
289
+
290
+ private convertResponse(response: OllamaResponse): LLMResponse {
291
+ const content = response.message.content;
292
+ const tool_calls: LLMToolCall[] = [];
293
+
294
+ if (response.message.tool_calls) {
295
+ for (const toolCall of response.message.tool_calls) {
296
+ const id = `ollama_${Date.now()}_${Math.random().toString(36).substring(7)}`;
297
+ tool_calls.push({
298
+ id,
299
+ name: toolCall.function.name,
300
+ arguments: toolCall.function.arguments,
301
+ });
302
+ }
303
+ }
304
+
305
+ return {
306
+ content,
307
+ tool_calls,
308
+ usage: {
309
+ input_tokens: response.prompt_eval_count || 0,
310
+ output_tokens: response.eval_count || 0,
311
+ },
312
+ model: response.model,
313
+ finish_reason: tool_calls.length > 0 ? 'tool_use' : 'stop',
314
+ };
315
+ }
316
+ }