opencandle 0.4.0 → 0.6.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 (564) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +186 -117
  3. package/dist/analysts/contracts.d.ts +1 -3
  4. package/dist/analysts/contracts.js +1 -11
  5. package/dist/analysts/contracts.js.map +1 -1
  6. package/dist/analysts/orchestrator.d.ts +1 -3
  7. package/dist/analysts/orchestrator.js +1 -26
  8. package/dist/analysts/orchestrator.js.map +1 -1
  9. package/dist/cli.js +32 -8
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config.d.ts +19 -3
  12. package/dist/config.js +69 -3
  13. package/dist/config.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/infra/browser.d.ts +1 -3
  18. package/dist/infra/browser.js +4 -2
  19. package/dist/infra/browser.js.map +1 -1
  20. package/dist/infra/cache.d.ts +8 -11
  21. package/dist/infra/cache.js +17 -15
  22. package/dist/infra/cache.js.map +1 -1
  23. package/dist/infra/http-client.d.ts +4 -1
  24. package/dist/infra/http-client.js +59 -6
  25. package/dist/infra/http-client.js.map +1 -1
  26. package/dist/infra/index.d.ts +3 -3
  27. package/dist/infra/index.js +3 -3
  28. package/dist/infra/index.js.map +1 -1
  29. package/dist/infra/native-dependencies.js +2 -2
  30. package/dist/infra/native-dependencies.js.map +1 -1
  31. package/dist/infra/node-version.js.map +1 -1
  32. package/dist/infra/opencandle-paths.d.ts +0 -3
  33. package/dist/infra/opencandle-paths.js +4 -11
  34. package/dist/infra/opencandle-paths.js.map +1 -1
  35. package/dist/infra/rate-limiter.d.ts +4 -0
  36. package/dist/infra/rate-limiter.js +17 -10
  37. package/dist/infra/rate-limiter.js.map +1 -1
  38. package/dist/market-state/alert-conditions.d.ts +34 -0
  39. package/dist/market-state/alert-conditions.js +23 -0
  40. package/dist/market-state/alert-conditions.js.map +1 -0
  41. package/dist/market-state/alert-runner.d.ts +55 -0
  42. package/dist/market-state/alert-runner.js +634 -0
  43. package/dist/market-state/alert-runner.js.map +1 -0
  44. package/dist/market-state/daily-report.d.ts +26 -0
  45. package/dist/market-state/daily-report.js +179 -0
  46. package/dist/market-state/daily-report.js.map +1 -0
  47. package/dist/market-state/local-automation-service.d.ts +25 -0
  48. package/dist/market-state/local-automation-service.js +119 -0
  49. package/dist/market-state/local-automation-service.js.map +1 -0
  50. package/dist/market-state/notification-delivery.d.ts +14 -0
  51. package/dist/market-state/notification-delivery.js +139 -0
  52. package/dist/market-state/notification-delivery.js.map +1 -0
  53. package/dist/market-state/resolve-for-mutation.d.ts +10 -0
  54. package/dist/market-state/resolve-for-mutation.js +15 -0
  55. package/dist/market-state/resolve-for-mutation.js.map +1 -0
  56. package/dist/market-state/resolve.d.ts +14 -0
  57. package/dist/market-state/resolve.js +89 -0
  58. package/dist/market-state/resolve.js.map +1 -0
  59. package/dist/market-state/service.d.ts +527 -0
  60. package/dist/market-state/service.js +1099 -0
  61. package/dist/market-state/service.js.map +1 -0
  62. package/dist/memory/index.d.ts +7 -7
  63. package/dist/memory/index.js +6 -6
  64. package/dist/memory/index.js.map +1 -1
  65. package/dist/memory/manager.d.ts +9 -0
  66. package/dist/memory/manager.js +39 -22
  67. package/dist/memory/manager.js.map +1 -1
  68. package/dist/memory/retrieval.js +7 -4
  69. package/dist/memory/retrieval.js.map +1 -1
  70. package/dist/memory/sqlite.js +385 -3
  71. package/dist/memory/sqlite.js.map +1 -1
  72. package/dist/memory/storage.d.ts +3 -2
  73. package/dist/memory/storage.js +1 -2
  74. package/dist/memory/storage.js.map +1 -1
  75. package/dist/memory/tool-defaults.js +64 -28
  76. package/dist/memory/tool-defaults.js.map +1 -1
  77. package/dist/memory/types.js +4 -0
  78. package/dist/memory/types.js.map +1 -1
  79. package/dist/monitor.d.ts +2 -0
  80. package/dist/monitor.js +104 -0
  81. package/dist/monitor.js.map +1 -0
  82. package/dist/onboarding/connect.js +4 -6
  83. package/dist/onboarding/connect.js.map +1 -1
  84. package/dist/onboarding/credential-interceptor.js +1 -1
  85. package/dist/onboarding/credential-interceptor.js.map +1 -1
  86. package/dist/onboarding/degradation-accumulator.js +1 -3
  87. package/dist/onboarding/degradation-accumulator.js.map +1 -1
  88. package/dist/onboarding/providers.js +3 -16
  89. package/dist/onboarding/providers.js.map +1 -1
  90. package/dist/onboarding/state.js.map +1 -1
  91. package/dist/onboarding/tool-helpers.js +1 -1
  92. package/dist/onboarding/tool-helpers.js.map +1 -1
  93. package/dist/onboarding/tool-tags.js +6 -4
  94. package/dist/onboarding/tool-tags.js.map +1 -1
  95. package/dist/onboarding/validation.js +1 -1
  96. package/dist/onboarding/validation.js.map +1 -1
  97. package/dist/pi/opencandle-extension.d.ts +8 -0
  98. package/dist/pi/opencandle-extension.js +637 -59
  99. package/dist/pi/opencandle-extension.js.map +1 -1
  100. package/dist/pi/session.d.ts +1 -1
  101. package/dist/pi/session.js +3 -1
  102. package/dist/pi/session.js.map +1 -1
  103. package/dist/pi/setup.js +17 -2
  104. package/dist/pi/setup.js.map +1 -1
  105. package/dist/pi/tool-adapter.js +5 -2
  106. package/dist/pi/tool-adapter.js.map +1 -1
  107. package/dist/prompts/context-builder.d.ts +18 -3
  108. package/dist/prompts/context-builder.js +117 -18
  109. package/dist/prompts/context-builder.js.map +1 -1
  110. package/dist/prompts/disclaimer.js +1 -1
  111. package/dist/prompts/disclaimer.js.map +1 -1
  112. package/dist/prompts/policy-cards.d.ts +13 -0
  113. package/dist/prompts/policy-cards.js +197 -0
  114. package/dist/prompts/policy-cards.js.map +1 -0
  115. package/dist/prompts/sections.d.ts +1 -1
  116. package/dist/prompts/sections.js +3 -3
  117. package/dist/prompts/sections.js.map +1 -1
  118. package/dist/prompts/symbol-preflight.d.ts +20 -0
  119. package/dist/prompts/symbol-preflight.js +49 -0
  120. package/dist/prompts/symbol-preflight.js.map +1 -0
  121. package/dist/prompts/workflow-prompts.d.ts +1 -1
  122. package/dist/prompts/workflow-prompts.js +209 -19
  123. package/dist/prompts/workflow-prompts.js.map +1 -1
  124. package/dist/providers/alpha-vantage.d.ts +1 -1
  125. package/dist/providers/alpha-vantage.js +49 -8
  126. package/dist/providers/alpha-vantage.js.map +1 -1
  127. package/dist/providers/coingecko.js +1 -1
  128. package/dist/providers/coingecko.js.map +1 -1
  129. package/dist/providers/errors.d.ts +5 -0
  130. package/dist/providers/errors.js +11 -0
  131. package/dist/providers/errors.js.map +1 -0
  132. package/dist/providers/exa-search.d.ts +2 -2
  133. package/dist/providers/exa-search.js +19 -11
  134. package/dist/providers/exa-search.js.map +1 -1
  135. package/dist/providers/fear-greed.js +1 -1
  136. package/dist/providers/fear-greed.js.map +1 -1
  137. package/dist/providers/finnhub.js +3 -5
  138. package/dist/providers/finnhub.js.map +1 -1
  139. package/dist/providers/fred.js +2 -2
  140. package/dist/providers/fred.js.map +1 -1
  141. package/dist/providers/index.d.ts +7 -6
  142. package/dist/providers/index.js +6 -5
  143. package/dist/providers/index.js.map +1 -1
  144. package/dist/providers/reddit.js +2 -2
  145. package/dist/providers/reddit.js.map +1 -1
  146. package/dist/providers/sec-edgar.d.ts +9 -1
  147. package/dist/providers/sec-edgar.js +181 -6
  148. package/dist/providers/sec-edgar.js.map +1 -1
  149. package/dist/providers/tradingview.d.ts +47 -0
  150. package/dist/providers/tradingview.js +275 -0
  151. package/dist/providers/tradingview.js.map +1 -0
  152. package/dist/providers/twitter.js +6 -8
  153. package/dist/providers/twitter.js.map +1 -1
  154. package/dist/providers/web-search.js +26 -12
  155. package/dist/providers/web-search.js.map +1 -1
  156. package/dist/providers/with-fallback.js +4 -2
  157. package/dist/providers/with-fallback.js.map +1 -1
  158. package/dist/providers/wrap-provider.d.ts +2 -3
  159. package/dist/providers/wrap-provider.js +14 -8
  160. package/dist/providers/wrap-provider.js.map +1 -1
  161. package/dist/providers/yahoo-finance.d.ts +3 -1
  162. package/dist/providers/yahoo-finance.js +226 -11
  163. package/dist/providers/yahoo-finance.js.map +1 -1
  164. package/dist/routing/classify-intent.d.ts +9 -0
  165. package/dist/routing/classify-intent.js +153 -3
  166. package/dist/routing/classify-intent.js.map +1 -1
  167. package/dist/routing/defaults.d.ts +1 -1
  168. package/dist/routing/defaults.js +3 -3
  169. package/dist/routing/defaults.js.map +1 -1
  170. package/dist/routing/entity-extractor.d.ts +2 -0
  171. package/dist/routing/entity-extractor.js +377 -26
  172. package/dist/routing/entity-extractor.js.map +1 -1
  173. package/dist/routing/fund-symbols.d.ts +2 -0
  174. package/dist/routing/fund-symbols.js +55 -0
  175. package/dist/routing/fund-symbols.js.map +1 -0
  176. package/dist/routing/horizon.d.ts +1 -0
  177. package/dist/routing/horizon.js +10 -0
  178. package/dist/routing/horizon.js.map +1 -0
  179. package/dist/routing/index.d.ts +12 -6
  180. package/dist/routing/index.js +8 -4
  181. package/dist/routing/index.js.map +1 -1
  182. package/dist/routing/legacy-rule-router.d.ts +9 -0
  183. package/dist/routing/legacy-rule-router.js +12 -0
  184. package/dist/routing/legacy-rule-router.js.map +1 -0
  185. package/dist/routing/planning.d.ts +54 -0
  186. package/dist/routing/planning.js +562 -0
  187. package/dist/routing/planning.js.map +1 -0
  188. package/dist/routing/route-manifest.d.ts +35 -0
  189. package/dist/routing/route-manifest.js +242 -0
  190. package/dist/routing/route-manifest.js.map +1 -0
  191. package/dist/routing/router-llm-client.js.map +1 -1
  192. package/dist/routing/router-prompt.js +46 -45
  193. package/dist/routing/router-prompt.js.map +1 -1
  194. package/dist/routing/router-types.d.ts +10 -0
  195. package/dist/routing/router.d.ts +1 -0
  196. package/dist/routing/router.js +572 -13
  197. package/dist/routing/router.js.map +1 -1
  198. package/dist/routing/slot-resolver.d.ts +1 -1
  199. package/dist/routing/slot-resolver.js +45 -7
  200. package/dist/routing/slot-resolver.js.map +1 -1
  201. package/dist/routing/symbol-disambiguator.d.ts +11 -0
  202. package/dist/routing/symbol-disambiguator.js +52 -0
  203. package/dist/routing/symbol-disambiguator.js.map +1 -0
  204. package/dist/routing/turn-context.d.ts +44 -0
  205. package/dist/routing/turn-context.js +45 -0
  206. package/dist/routing/turn-context.js.map +1 -0
  207. package/dist/routing/types.d.ts +15 -1
  208. package/dist/runtime/answer-contracts.d.ts +82 -0
  209. package/dist/runtime/answer-contracts.js +442 -0
  210. package/dist/runtime/answer-contracts.js.map +1 -0
  211. package/dist/runtime/artifact-contracts.d.ts +14 -0
  212. package/dist/runtime/artifact-contracts.js +57 -0
  213. package/dist/runtime/artifact-contracts.js.map +1 -0
  214. package/dist/runtime/planning-evidence.d.ts +99 -0
  215. package/dist/runtime/planning-evidence.js +466 -0
  216. package/dist/runtime/planning-evidence.js.map +1 -0
  217. package/dist/runtime/prompt-step.d.ts +1 -9
  218. package/dist/runtime/prompt-step.js +0 -10
  219. package/dist/runtime/prompt-step.js.map +1 -1
  220. package/dist/runtime/run-context.d.ts +5 -2
  221. package/dist/runtime/run-context.js +8 -1
  222. package/dist/runtime/run-context.js.map +1 -1
  223. package/dist/runtime/session-coordinator.d.ts +29 -3
  224. package/dist/runtime/session-coordinator.js +204 -31
  225. package/dist/runtime/session-coordinator.js.map +1 -1
  226. package/dist/runtime/session-title.d.ts +14 -0
  227. package/dist/runtime/session-title.js +50 -0
  228. package/dist/runtime/session-title.js.map +1 -0
  229. package/dist/runtime/tool-defaults-wrapper.js +1 -3
  230. package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
  231. package/dist/runtime/validation.js.map +1 -1
  232. package/dist/runtime/workflow-events.js.map +1 -1
  233. package/dist/runtime/workflow-runner.d.ts +3 -3
  234. package/dist/runtime/workflow-runner.js +1 -1
  235. package/dist/runtime/workflow-runner.js.map +1 -1
  236. package/dist/sentiment/adapters/finnhub.d.ts +1 -1
  237. package/dist/sentiment/adapters/finnhub.js +6 -1
  238. package/dist/sentiment/adapters/finnhub.js.map +1 -1
  239. package/dist/sentiment/adapters/reddit.d.ts +2 -2
  240. package/dist/sentiment/adapters/twitter.d.ts +1 -1
  241. package/dist/sentiment/adapters/web.d.ts +1 -1
  242. package/dist/sentiment/index.d.ts +9 -11
  243. package/dist/sentiment/index.js +9 -20
  244. package/dist/sentiment/index.js.map +1 -1
  245. package/dist/sentiment/keywords.js +26 -4
  246. package/dist/sentiment/keywords.js.map +1 -1
  247. package/dist/sentiment/pipeline.d.ts +2 -2
  248. package/dist/sentiment/pipeline.js +1 -1
  249. package/dist/sentiment/pipeline.js.map +1 -1
  250. package/dist/sentiment/scorer.js +1 -1
  251. package/dist/sentiment/store.d.ts +1 -1
  252. package/dist/sentiment/store.js +1 -1
  253. package/dist/sentiment/store.js.map +1 -1
  254. package/dist/sentiment/trends.d.ts +1 -1
  255. package/dist/sentiment/trends.js.map +1 -1
  256. package/dist/sentiment/types.js.map +1 -1
  257. package/dist/system-prompt.js +7 -3
  258. package/dist/system-prompt.js.map +1 -1
  259. package/dist/tool-kit.d.ts +7 -7
  260. package/dist/tool-kit.js +4 -4
  261. package/dist/tool-kit.js.map +1 -1
  262. package/dist/tools/fundamentals/company-overview.js +12 -7
  263. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  264. package/dist/tools/fundamentals/comps.js +19 -10
  265. package/dist/tools/fundamentals/comps.js.map +1 -1
  266. package/dist/tools/fundamentals/dcf.js +24 -12
  267. package/dist/tools/fundamentals/dcf.js.map +1 -1
  268. package/dist/tools/fundamentals/earnings.js +9 -4
  269. package/dist/tools/fundamentals/earnings.js.map +1 -1
  270. package/dist/tools/fundamentals/financials.js +9 -4
  271. package/dist/tools/fundamentals/financials.js.map +1 -1
  272. package/dist/tools/fundamentals/sec-filings.d.ts +1 -0
  273. package/dist/tools/fundamentals/sec-filings.js +36 -4
  274. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  275. package/dist/tools/index.d.ts +23 -18
  276. package/dist/tools/index.js +53 -38
  277. package/dist/tools/index.js.map +1 -1
  278. package/dist/tools/interaction/ask-user.js +15 -3
  279. package/dist/tools/interaction/ask-user.js.map +1 -1
  280. package/dist/tools/interaction/twitter-login.js +13 -3
  281. package/dist/tools/interaction/twitter-login.js.map +1 -1
  282. package/dist/tools/macro/fear-greed.js +1 -1
  283. package/dist/tools/macro/fear-greed.js.map +1 -1
  284. package/dist/tools/macro/fred-data.d.ts +1 -1
  285. package/dist/tools/macro/fred-data.js +44 -9
  286. package/dist/tools/macro/fred-data.js.map +1 -1
  287. package/dist/tools/market/crypto-history.js +21 -3
  288. package/dist/tools/market/crypto-history.js.map +1 -1
  289. package/dist/tools/market/crypto-price.js +4 -2
  290. package/dist/tools/market/crypto-price.js.map +1 -1
  291. package/dist/tools/market/screen-stocks.d.ts +18 -0
  292. package/dist/tools/market/screen-stocks.js +252 -0
  293. package/dist/tools/market/screen-stocks.js.map +1 -0
  294. package/dist/tools/market/search-ticker.js +161 -9
  295. package/dist/tools/market/search-ticker.js.map +1 -1
  296. package/dist/tools/market/stock-history.d.ts +2 -2
  297. package/dist/tools/market/stock-history.js +27 -8
  298. package/dist/tools/market/stock-history.js.map +1 -1
  299. package/dist/tools/market/stock-quote.js +6 -4
  300. package/dist/tools/market/stock-quote.js.map +1 -1
  301. package/dist/tools/options/greeks.js +1 -2
  302. package/dist/tools/options/greeks.js.map +1 -1
  303. package/dist/tools/options/option-chain.js +27 -9
  304. package/dist/tools/options/option-chain.js.map +1 -1
  305. package/dist/tools/portfolio/alerts.d.ts +15 -0
  306. package/dist/tools/portfolio/alerts.js +357 -0
  307. package/dist/tools/portfolio/alerts.js.map +1 -0
  308. package/dist/tools/portfolio/correlation.d.ts +1 -1
  309. package/dist/tools/portfolio/correlation.js +34 -14
  310. package/dist/tools/portfolio/correlation.js.map +1 -1
  311. package/dist/tools/portfolio/daily-report.d.ts +8 -0
  312. package/dist/tools/portfolio/daily-report.js +83 -0
  313. package/dist/tools/portfolio/daily-report.js.map +1 -0
  314. package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
  315. package/dist/tools/portfolio/holdings-overlap.js +112 -0
  316. package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
  317. package/dist/tools/portfolio/notifications.d.ts +7 -0
  318. package/dist/tools/portfolio/notifications.js +43 -0
  319. package/dist/tools/portfolio/notifications.js.map +1 -0
  320. package/dist/tools/portfolio/predictions.d.ts +12 -6
  321. package/dist/tools/portfolio/predictions.js +338 -88
  322. package/dist/tools/portfolio/predictions.js.map +1 -1
  323. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  324. package/dist/tools/portfolio/risk-analysis.js +46 -7
  325. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  326. package/dist/tools/portfolio/tracker.d.ts +4 -3
  327. package/dist/tools/portfolio/tracker.js +247 -102
  328. package/dist/tools/portfolio/tracker.js.map +1 -1
  329. package/dist/tools/portfolio/watchlist.d.ts +6 -4
  330. package/dist/tools/portfolio/watchlist.js +209 -101
  331. package/dist/tools/portfolio/watchlist.js.map +1 -1
  332. package/dist/tools/sentiment/reddit-sentiment.js +24 -11
  333. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  334. package/dist/tools/sentiment/sentiment-summary.js +71 -14
  335. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  336. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  337. package/dist/tools/sentiment/sentiment-trend.js +12 -2
  338. package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
  339. package/dist/tools/sentiment/twitter-sentiment.js +13 -6
  340. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  341. package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
  342. package/dist/tools/sentiment/untrusted-text.js +17 -0
  343. package/dist/tools/sentiment/untrusted-text.js.map +1 -0
  344. package/dist/tools/sentiment/web-search.js +37 -12
  345. package/dist/tools/sentiment/web-search.js.map +1 -1
  346. package/dist/tools/sentiment/web-sentiment.js +16 -4
  347. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  348. package/dist/tools/technical/backtest.d.ts +3 -3
  349. package/dist/tools/technical/backtest.js +65 -44
  350. package/dist/tools/technical/backtest.js.map +1 -1
  351. package/dist/tools/technical/indicators.js +24 -8
  352. package/dist/tools/technical/indicators.js.map +1 -1
  353. package/dist/types/index.d.ts +3 -3
  354. package/dist/types/index.js.map +1 -1
  355. package/dist/types/market.d.ts +1 -0
  356. package/dist/types/options.d.ts +10 -0
  357. package/dist/types/portfolio.d.ts +41 -4
  358. package/dist/workflows/compare-assets.d.ts +0 -3
  359. package/dist/workflows/compare-assets.js +55 -10
  360. package/dist/workflows/compare-assets.js.map +1 -1
  361. package/dist/workflows/index.d.ts +3 -4
  362. package/dist/workflows/index.js +3 -3
  363. package/dist/workflows/index.js.map +1 -1
  364. package/dist/workflows/options-screener.d.ts +0 -3
  365. package/dist/workflows/options-screener.js +88 -14
  366. package/dist/workflows/options-screener.js.map +1 -1
  367. package/dist/workflows/portfolio-builder.d.ts +0 -3
  368. package/dist/workflows/portfolio-builder.js +7 -11
  369. package/dist/workflows/portfolio-builder.js.map +1 -1
  370. package/gui/server/ask-user-bridge.ts +82 -0
  371. package/gui/server/automation-heartbeat.ts +97 -0
  372. package/gui/server/background-quotes.ts +97 -1
  373. package/gui/server/chat-event-adapter.ts +32 -10
  374. package/gui/server/chat-run-session.ts +16 -0
  375. package/gui/server/gui-session-manager.ts +5 -0
  376. package/gui/server/invoke-tool.ts +144 -1
  377. package/gui/server/live-chat-event-adapter.ts +21 -6
  378. package/gui/server/market-state-api.ts +315 -0
  379. package/gui/server/model-setup.ts +149 -2
  380. package/gui/server/private-api-access.ts +62 -0
  381. package/gui/server/projector.ts +58 -11
  382. package/gui/server/prompt-observation.ts +58 -0
  383. package/gui/server/quote-snapshot-store.ts +50 -0
  384. package/gui/server/server.ts +236 -376
  385. package/gui/server/session-actions.ts +186 -1
  386. package/gui/server/session-entry-wait.ts +81 -0
  387. package/gui/server/shutdown.ts +47 -0
  388. package/gui/server/tool-invoke-ack.ts +49 -0
  389. package/gui/server/tool-metadata.ts +23 -10
  390. package/gui/server/websocket.ts +13 -3
  391. package/gui/server/writer-lock.ts +6 -2
  392. package/gui/server/ws-hub.ts +292 -0
  393. package/gui/shared/chat-events.ts +16 -1
  394. package/gui/shared/event-reducer.ts +24 -6
  395. package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +1 -0
  396. package/gui/web/dist/assets/index-2KZtKBmu.css +1 -0
  397. package/gui/web/dist/assets/index-CveNgtDg.js +69 -0
  398. package/gui/web/dist/index.html +2 -2
  399. package/package.json +22 -12
  400. package/src/analysts/contracts.ts +10 -23
  401. package/src/analysts/orchestrator.ts +8 -43
  402. package/src/cli.ts +37 -13
  403. package/src/config.ts +99 -7
  404. package/src/index.ts +1 -1
  405. package/src/infra/browser.ts +4 -2
  406. package/src/infra/cache.ts +41 -30
  407. package/src/infra/http-client.ts +72 -6
  408. package/src/infra/index.ts +7 -10
  409. package/src/infra/native-dependencies.ts +8 -3
  410. package/src/infra/node-version.ts +3 -1
  411. package/src/infra/opencandle-paths.ts +3 -14
  412. package/src/infra/rate-limiter.ts +32 -20
  413. package/src/market-state/alert-conditions.ts +82 -0
  414. package/src/market-state/alert-runner.ts +863 -0
  415. package/src/market-state/daily-report.ts +247 -0
  416. package/src/market-state/local-automation-service.ts +162 -0
  417. package/src/market-state/notification-delivery.ts +158 -0
  418. package/src/market-state/resolve-for-mutation.ts +24 -0
  419. package/src/market-state/resolve.ts +112 -0
  420. package/src/market-state/service.ts +2344 -0
  421. package/src/memory/index.ts +7 -7
  422. package/src/memory/manager.ts +57 -26
  423. package/src/memory/retrieval.ts +8 -7
  424. package/src/memory/sqlite.ts +407 -6
  425. package/src/memory/storage.ts +8 -17
  426. package/src/memory/tool-defaults.ts +60 -39
  427. package/src/memory/types.ts +7 -3
  428. package/src/monitor.ts +121 -0
  429. package/src/onboarding/connect.ts +10 -33
  430. package/src/onboarding/credential-interceptor.ts +3 -15
  431. package/src/onboarding/degradation-accumulator.ts +1 -3
  432. package/src/onboarding/providers.ts +9 -40
  433. package/src/onboarding/state.ts +4 -15
  434. package/src/onboarding/tool-helpers.ts +2 -9
  435. package/src/onboarding/tool-tags.ts +6 -6
  436. package/src/onboarding/validation.ts +14 -20
  437. package/src/pi/opencandle-extension.ts +795 -120
  438. package/src/pi/session.ts +7 -5
  439. package/src/pi/setup.ts +61 -33
  440. package/src/pi/tool-adapter.ts +5 -2
  441. package/src/prompts/context-builder.ts +143 -21
  442. package/src/prompts/disclaimer.ts +1 -1
  443. package/src/prompts/policy-cards.ts +220 -0
  444. package/src/prompts/sections.ts +4 -4
  445. package/src/prompts/symbol-preflight.ts +80 -0
  446. package/src/prompts/workflow-prompts.ts +231 -28
  447. package/src/providers/alpha-vantage.ts +82 -40
  448. package/src/providers/coingecko.ts +2 -5
  449. package/src/providers/errors.ts +9 -0
  450. package/src/providers/exa-search.ts +24 -22
  451. package/src/providers/fear-greed.ts +1 -1
  452. package/src/providers/finnhub.ts +7 -6
  453. package/src/providers/fred.ts +3 -3
  454. package/src/providers/index.ts +14 -6
  455. package/src/providers/reddit.ts +17 -6
  456. package/src/providers/sec-edgar.ts +235 -5
  457. package/src/providers/tradingview.ts +399 -0
  458. package/src/providers/twitter.ts +6 -8
  459. package/src/providers/web-search.ts +30 -20
  460. package/src/providers/with-fallback.ts +8 -7
  461. package/src/providers/wrap-provider.ts +15 -10
  462. package/src/providers/yahoo-finance.ts +292 -20
  463. package/src/routing/classify-intent.ts +186 -4
  464. package/src/routing/defaults.ts +4 -4
  465. package/src/routing/entity-extractor.ts +428 -28
  466. package/src/routing/fund-symbols.ts +58 -0
  467. package/src/routing/horizon.ts +7 -0
  468. package/src/routing/index.ts +60 -16
  469. package/src/routing/legacy-rule-router.ts +13 -0
  470. package/src/routing/planning.ts +823 -0
  471. package/src/routing/route-manifest.ts +309 -0
  472. package/src/routing/router-llm-client.ts +4 -4
  473. package/src/routing/router-prompt.ts +52 -52
  474. package/src/routing/router-types.ts +18 -0
  475. package/src/routing/router.ts +717 -20
  476. package/src/routing/slot-resolver.ts +75 -14
  477. package/src/routing/symbol-disambiguator.ts +72 -0
  478. package/src/routing/turn-context.ts +108 -0
  479. package/src/routing/types.ts +15 -1
  480. package/src/runtime/answer-contracts.ts +672 -0
  481. package/src/runtime/artifact-contracts.ts +77 -0
  482. package/src/runtime/planning-evidence.ts +682 -0
  483. package/src/runtime/prompt-step.ts +1 -16
  484. package/src/runtime/run-context.ts +12 -2
  485. package/src/runtime/session-coordinator.ts +297 -56
  486. package/src/runtime/session-title.ts +60 -0
  487. package/src/runtime/tool-defaults-wrapper.ts +1 -3
  488. package/src/runtime/validation.ts +1 -4
  489. package/src/runtime/workflow-events.ts +7 -7
  490. package/src/runtime/workflow-runner.ts +5 -11
  491. package/src/sentiment/adapters/finnhub.ts +7 -2
  492. package/src/sentiment/adapters/reddit.ts +2 -2
  493. package/src/sentiment/adapters/twitter.ts +1 -1
  494. package/src/sentiment/adapters/web.ts +1 -1
  495. package/src/sentiment/index.ts +16 -26
  496. package/src/sentiment/keywords.ts +26 -4
  497. package/src/sentiment/pipeline.ts +15 -4
  498. package/src/sentiment/scorer.ts +1 -1
  499. package/src/sentiment/store.ts +2 -2
  500. package/src/sentiment/trends.ts +9 -3
  501. package/src/sentiment/types.ts +5 -4
  502. package/src/system-prompt.ts +7 -3
  503. package/src/tool-kit.ts +10 -9
  504. package/src/tools/fundamentals/company-overview.ts +20 -10
  505. package/src/tools/fundamentals/comps.ts +69 -56
  506. package/src/tools/fundamentals/dcf.ts +146 -96
  507. package/src/tools/fundamentals/earnings.ts +17 -7
  508. package/src/tools/fundamentals/financials.ts +17 -8
  509. package/src/tools/fundamentals/sec-filings.ts +52 -8
  510. package/src/tools/index.ts +53 -38
  511. package/src/tools/interaction/ask-user.ts +22 -10
  512. package/src/tools/interaction/twitter-login.ts +17 -5
  513. package/src/tools/macro/fear-greed.ts +2 -2
  514. package/src/tools/macro/fred-data.ts +80 -42
  515. package/src/tools/market/crypto-history.ts +25 -4
  516. package/src/tools/market/crypto-price.ts +7 -7
  517. package/src/tools/market/screen-stocks.ts +279 -0
  518. package/src/tools/market/search-ticker.ts +219 -18
  519. package/src/tools/market/stock-history.ts +38 -13
  520. package/src/tools/market/stock-quote.ts +11 -8
  521. package/src/tools/options/greeks.ts +5 -6
  522. package/src/tools/options/option-chain.ts +47 -18
  523. package/src/tools/portfolio/alerts.ts +457 -0
  524. package/src/tools/portfolio/correlation.ts +48 -21
  525. package/src/tools/portfolio/daily-report.ts +101 -0
  526. package/src/tools/portfolio/holdings-overlap.ts +139 -0
  527. package/src/tools/portfolio/notifications.ts +45 -0
  528. package/src/tools/portfolio/predictions.ts +407 -107
  529. package/src/tools/portfolio/risk-analysis.ts +47 -8
  530. package/src/tools/portfolio/tracker.ts +271 -110
  531. package/src/tools/portfolio/watchlist.ts +251 -116
  532. package/src/tools/sentiment/reddit-sentiment.ts +51 -25
  533. package/src/tools/sentiment/sentiment-summary.ts +116 -35
  534. package/src/tools/sentiment/sentiment-trend.ts +24 -7
  535. package/src/tools/sentiment/twitter-sentiment.ts +23 -16
  536. package/src/tools/sentiment/untrusted-text.ts +21 -0
  537. package/src/tools/sentiment/web-search.ts +52 -16
  538. package/src/tools/sentiment/web-sentiment.ts +27 -11
  539. package/src/tools/technical/backtest.ts +78 -47
  540. package/src/tools/technical/indicators.ts +40 -17
  541. package/src/types/index.ts +8 -3
  542. package/src/types/market.ts +1 -0
  543. package/src/types/options.ts +17 -0
  544. package/src/types/portfolio.ts +46 -4
  545. package/src/types/sentiment.ts +2 -2
  546. package/src/workflows/compare-assets.ts +67 -19
  547. package/src/workflows/index.ts +3 -4
  548. package/src/workflows/options-screener.ts +98 -22
  549. package/src/workflows/portfolio-builder.ts +40 -29
  550. package/dist/runtime/index.d.ts +0 -16
  551. package/dist/runtime/index.js +0 -10
  552. package/dist/runtime/index.js.map +0 -1
  553. package/dist/runtime/provider-ids.d.ts +0 -14
  554. package/dist/runtime/provider-ids.js +0 -14
  555. package/dist/runtime/provider-ids.js.map +0 -1
  556. package/dist/workflows/types.d.ts +0 -4
  557. package/dist/workflows/types.js +0 -2
  558. package/dist/workflows/types.js.map +0 -1
  559. package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +0 -1
  560. package/gui/web/dist/assets/index-DBrWq43L.css +0 -1
  561. package/gui/web/dist/assets/index-RflHaj0y.js +0 -67
  562. package/src/runtime/index.ts +0 -55
  563. package/src/runtime/provider-ids.ts +0 -15
  564. package/src/workflows/types.ts +0 -4
@@ -1,52 +1,57 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
+ buildComprehensiveAnalysisDefinition,
3
4
  isAnalysisRequest,
4
5
  normalizeSymbol,
5
6
  } from "../analysts/orchestrator.js";
6
- import { buildComprehensiveAnalysisDefinition } from "../analysts/orchestrator.js";
7
7
  import { getConfig } from "../config.js";
8
+ import type { InstrumentCandidate } from "../market-state/resolve.js";
9
+ import { runProviderConnect } from "../onboarding/connect.js";
10
+ import { resolveCredentialRequired } from "../onboarding/credential-interceptor.js";
11
+ import { createDegradationAccumulator } from "../onboarding/degradation-accumulator.js";
12
+ import { promptUser } from "../onboarding/prompt-user.js";
13
+ import { getProvider, type ProviderId } from "../onboarding/providers.js";
14
+ import {
15
+ loadOnboardingState,
16
+ markProviderNeverAsk,
17
+ markProviderSnoozed,
18
+ markWelcomeShown,
19
+ saveOnboardingState,
20
+ shouldShowWelcome,
21
+ } from "../onboarding/state.js";
22
+ import { buildConnectedTag, buildSkippedTag, parseToolTag } from "../onboarding/tool-tags.js";
23
+ import { DISCLAIMER_TEXT } from "../prompts/disclaimer.js";
24
+ import { formatPreflightDropAnnotation, preflightSymbols } from "../prompts/symbol-preflight.js";
25
+ import { buildAssumptionsBlockFromRouter } from "../prompts/workflow-prompts.js";
8
26
  import {
9
- classifyIntent,
27
+ buildResolvedTurnContext,
28
+ classifyWithLegacyRules,
10
29
  createPiAiRouterClient,
30
+ hasFinanceSignals,
11
31
  resolveOptionsScreenerSlots,
12
32
  resolvePortfolioSlots,
13
33
  route as routeLlm,
14
34
  } from "../routing/index.js";
35
+ import type { RouterInputContext, RouterLlmClient, RouterOutput } from "../routing/router-types.js";
36
+ import { disambiguateSymbols } from "../routing/symbol-disambiguator.js";
37
+ import type { ResolvedTurnContext } from "../routing/turn-context.js";
15
38
  import type {
16
- RouterInputContext,
17
- RouterLlmClient,
18
- RouterOutput,
19
- } from "../routing/router-types.js";
20
- import type { CompareAssetsSlots, SlotResolution } from "../routing/types.js";
21
- import { buildAssumptionsBlockFromRouter } from "../prompts/workflow-prompts.js";
39
+ CompareAssetsSlots,
40
+ ExtractedEntities,
41
+ SlotResolution,
42
+ SlotSource,
43
+ } from "../routing/types.js";
44
+ import { SessionCoordinator } from "../runtime/session-coordinator.js";
45
+ import { generateSessionTitle } from "../runtime/session-title.js";
46
+ import { registerAskUserTool } from "../tools/interaction/ask-user.js";
47
+ import { registerTwitterLoginTool } from "../tools/interaction/twitter-login.js";
48
+ import type { AskUserHandler } from "../types/index.js";
22
49
  import {
23
- buildPortfolioWorkflowDefinition,
24
- buildOptionsScreenerWorkflowDefinition,
25
50
  buildCompareAssetsWorkflowDefinition,
51
+ buildOptionsScreenerWorkflowDefinition,
52
+ buildPortfolioWorkflowDefinition,
26
53
  } from "../workflows/index.js";
27
54
  import { getOpenCandleToolDefinitions } from "./tool-adapter.js";
28
- import { registerAskUserTool } from "../tools/interaction/ask-user.js";
29
- import { registerTwitterLoginTool } from "../tools/interaction/twitter-login.js";
30
- import { SessionCoordinator } from "../runtime/session-coordinator.js";
31
- import {
32
- getProvider,
33
- type ProviderId,
34
- } from "../onboarding/providers.js";
35
- import {
36
- loadOnboardingState,
37
- saveOnboardingState,
38
- markProviderSnoozed,
39
- markProviderNeverAsk,
40
- markWelcomeShown,
41
- shouldShowWelcome,
42
- } from "../onboarding/state.js";
43
- import { parseToolTag, buildSkippedTag, buildConnectedTag } from "../onboarding/tool-tags.js";
44
- import { resolveCredentialRequired } from "../onboarding/credential-interceptor.js";
45
- import { createDegradationAccumulator } from "../onboarding/degradation-accumulator.js";
46
- import { promptUser } from "../onboarding/prompt-user.js";
47
- import { runProviderConnect } from "../onboarding/connect.js";
48
- import type { AskUserHandler } from "../types/index.js";
49
- import { DISCLAIMER_TEXT } from "../prompts/disclaimer.js";
50
55
 
51
56
  export interface OpenCandleExtensionOptions {
52
57
  askUserHandler?: AskUserHandler;
@@ -55,11 +60,27 @@ export interface OpenCandleExtensionOptions {
55
60
  * of the pi-ai-backed default. Intended for tests + offline eval runners.
56
61
  */
57
62
  routerLlmClient?: RouterLlmClient;
63
+ symbolSearch?: (query: string) => Promise<InstrumentCandidate[]>;
64
+ /**
65
+ * Optional completion function for LLM session titles. When omitted, the
66
+ * extension resolves a pi-ai client from the session's current model.
67
+ * Intended for tests + offline runners.
68
+ */
69
+ titleCompletion?: (prompt: string) => Promise<string>;
58
70
  }
59
71
 
60
- export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCandleExtensionOptions): void {
72
+ export default function openCandleExtension(
73
+ pi: ExtensionAPI,
74
+ options?: OpenCandleExtensionOptions,
75
+ ): void {
61
76
  const coordinator = new SessionCoordinator();
62
77
 
78
+ // Workflow transforms replace the user's turn with the expanded prompt; this
79
+ // marker lets the GUI render the user's original words instead.
80
+ const markOriginalInput = (original: string): void => {
81
+ pi.appendEntry("opencandle-user-input", { original });
82
+ };
83
+
63
84
  // Credential-interception state. Lifetime:
64
85
  // `sessionPromptedSet` — cleared on session_start, persists across turns
65
86
  // within a session so users don't get re-prompted after picking
@@ -76,6 +97,12 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
76
97
  const sessionPromptedSet = new Set<ProviderId>();
77
98
  let hardPromptFiredInWorkflow = false;
78
99
  const degradationAccumulator = createDegradationAccumulator();
100
+ let activeToolSnapshot: string[] | null = null;
101
+ let currentRouteToolContext: ResolvedTurnContext | null = null;
102
+ // LLM session-title state: one title attempt per session per process.
103
+ // Reset on session_start; set before the (async) title call fires so
104
+ // overlapping turn_end events cannot double-title.
105
+ let sessionTitleAttempted = false;
79
106
 
80
107
  // Register tools
81
108
  for (const tool of getOpenCandleToolDefinitions()) {
@@ -93,7 +120,9 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
93
120
  ctx.ui.notify("Usage: /analyze <ticker>", "warning");
94
121
  return;
95
122
  }
96
- const definition = buildComprehensiveAnalysisDefinition(symbol, { debate: getConfig().debate });
123
+ const definition = buildComprehensiveAnalysisDefinition(symbol, {
124
+ debate: getConfig().debate,
125
+ });
97
126
  coordinator.executeWorkflow(pi, definition, ctx);
98
127
  },
99
128
  });
@@ -156,10 +185,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
156
185
  const all = listAllProviders()
157
186
  .map((p) => ` ${p.displayName} (${p.aliases.join(", ")})`)
158
187
  .join("\n");
159
- ctx.ui.notify(
160
- `Unknown provider: "${trimmed}". Available:\n${all}`,
161
- "warning",
162
- );
188
+ ctx.ui.notify(`Unknown provider: "${trimmed}". Available:\n${all}`, "warning");
163
189
  return;
164
190
  }
165
191
  if (Array.isArray(resolved)) {
@@ -190,6 +216,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
190
216
  coordinator.initSession(ctx.sessionManager.getSessionId());
191
217
  sessionPromptedSet.clear();
192
218
  hardPromptFiredInWorkflow = false;
219
+ sessionTitleAttempted = false;
193
220
 
194
221
  if (!ctx.hasUI) return;
195
222
  // Pin the user-facing disclaimer in the UI footer for the entire session.
@@ -257,10 +284,10 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
257
284
  // footer status pinned at session_start is the primary user-visible channel.
258
285
  pi.on("turn_end", async (event) => {
259
286
  const msg = event.message;
260
- const isFinalAssistantTurn =
261
- msg.role === "assistant" && msg.stopReason === "stop";
287
+ const isFinalAssistantTurn = msg.role === "assistant" && msg.stopReason === "stop";
262
288
  if (isFinalAssistantTurn) {
263
289
  pi.appendEntry("opencandle-disclaimer", { text: DISCLAIMER_TEXT });
290
+ restoreRouteToolScope();
264
291
  }
265
292
 
266
293
  if (degradationAccumulator.isEmpty()) return;
@@ -272,6 +299,82 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
272
299
  degradationAccumulator.reset();
273
300
  });
274
301
 
302
+ // LLM session titles. After the first completed user↔assistant exchange,
303
+ // replace the placeholder session name (the raw first prompt, set by the
304
+ // GUI server / Pi's firstMessage fallback) with a short model-written
305
+ // summary. Manual renames are left alone, and each session is attempted at
306
+ // most once per process. Model failures are swallowed (placeholder stays)
307
+ // but recorded as an `opencandle-title-error` entry for observability.
308
+ pi.on("turn_end", async (event, ctx) => {
309
+ if (sessionTitleAttempted) return;
310
+ const msg = event.message as { role?: unknown; stopReason?: unknown };
311
+ if (msg?.role !== "assistant" || msg?.stopReason !== "stop") return;
312
+ const sessionManager = ctx?.sessionManager;
313
+ if (typeof sessionManager?.getBranch !== "function") return;
314
+
315
+ const branch = sessionManager.getBranch();
316
+ let firstUserText: string | null = null;
317
+ let firstAssistantText: string | null = null;
318
+ let originalInput: string | null = null;
319
+ for (const entry of branch) {
320
+ if (
321
+ originalInput === null &&
322
+ entry.type === "custom" &&
323
+ (entry as { customType?: unknown }).customType === "opencandle-user-input"
324
+ ) {
325
+ const original = (entry as { data?: { original?: unknown } }).data?.original;
326
+ if (typeof original === "string" && original.trim().length > 0) {
327
+ originalInput = original;
328
+ }
329
+ continue;
330
+ }
331
+ if (entry.type !== "message") continue;
332
+ const message = (entry as { message?: { role?: unknown; content?: unknown } }).message;
333
+ const text = extractMessageText(message?.content);
334
+ if (text.trim().length === 0) continue;
335
+ if (firstUserText === null && message?.role === "user") firstUserText = text;
336
+ if (firstAssistantText === null && message?.role === "assistant") {
337
+ firstAssistantText = text;
338
+ }
339
+ if (firstUserText !== null && firstAssistantText !== null) break;
340
+ }
341
+ // Fall back to the just-finished assistant message when the branch has
342
+ // not surfaced it yet at turn_end time.
343
+ if (firstAssistantText === null) {
344
+ const eventText = extractMessageText((msg as { content?: unknown }).content);
345
+ if (eventText.trim().length > 0) firstAssistantText = eventText;
346
+ }
347
+ if (firstUserText === null || firstAssistantText === null) return;
348
+
349
+ const userText = originalInput ?? firstUserText;
350
+ // A manual rename must be left alone — only replace the placeholder
351
+ // (unset, the raw first prompt, or the GUI's "first 77 chars + ..." form).
352
+ if (!isPlaceholderSessionName(pi.getSessionName(), [userText, firstUserText])) return;
353
+
354
+ const completion =
355
+ options?.titleCompletion ??
356
+ (() => {
357
+ const client = options?.routerLlmClient ?? resolveRouterLlmClient(ctx);
358
+ return client ? (prompt: string) => client.complete(prompt) : null;
359
+ })();
360
+ if (!completion) return;
361
+
362
+ sessionTitleAttempted = true;
363
+ try {
364
+ const title = await generateSessionTitle(
365
+ { userText, assistantText: firstAssistantText.slice(0, 500) },
366
+ completion,
367
+ );
368
+ if (title !== null) {
369
+ pi.setSessionName(title);
370
+ }
371
+ } catch (err) {
372
+ pi.appendEntry("opencandle-title-error", {
373
+ message: err instanceof Error ? err.message : String(err),
374
+ });
375
+ }
376
+ });
377
+
275
378
  // Intercept tool results for credential-required and soft-degraded tags.
276
379
  pi.on("tool_result", async (event, ctx) => {
277
380
  // First pass: record any soft-degradation tags in the per-turn accumulator
@@ -336,10 +439,9 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
336
439
  // action === "prompt": pause and ask the user via promptUser.
337
440
  const descriptor = getProvider(parsed.provider);
338
441
  const connectLabel = `Connect now — ${descriptor.instructionsHint}`;
339
- const continueLabel =
340
- descriptor.fallbackDescription
341
- ? `Continue with ${descriptor.fallbackDescription} for this run`
342
- : `Continue without ${descriptor.displayName} for this run`;
442
+ const continueLabel = descriptor.fallbackDescription
443
+ ? `Continue with ${descriptor.fallbackDescription} for this run`
444
+ : `Continue without ${descriptor.displayName} for this run`;
343
445
  const snoozeLabel = `Snooze ${descriptor.snoozeDurationDays} days`;
344
446
  const neverLabel = `Never ask again`;
345
447
  const questionBody =
@@ -368,12 +470,11 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
368
470
  content: [
369
471
  {
370
472
  type: "text",
371
- text:
372
- `${buildSkippedTag({
373
- provider: parsed.provider,
374
- reason: "credential_not_provided",
375
- remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
376
- })}\n\nPrompt was cancelled.`,
473
+ text: `${buildSkippedTag({
474
+ provider: parsed.provider,
475
+ reason: "credential_not_provided",
476
+ remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
477
+ })}\n\nPrompt was cancelled.`,
377
478
  },
378
479
  ],
379
480
  };
@@ -411,12 +512,11 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
411
512
  content: [
412
513
  {
413
514
  type: "text",
414
- text:
415
- `${buildSkippedTag({
416
- provider: parsed.provider,
417
- reason: "credential_not_provided",
418
- remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
419
- })}\n\n${descriptor.displayName} connect was ${connectOutcomeDescription}.`,
515
+ text: `${buildSkippedTag({
516
+ provider: parsed.provider,
517
+ reason: "credential_not_provided",
518
+ remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
519
+ })}\n\n${descriptor.displayName} connect was ${connectOutcomeDescription}.`,
420
520
  },
421
521
  ],
422
522
  };
@@ -438,13 +538,12 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
438
538
  content: [
439
539
  {
440
540
  type: "text",
441
- text:
442
- `${buildSkippedTag({
443
- provider: parsed.provider,
444
- reason: "credential_not_provided",
445
- remediation,
446
- silenced,
447
- })}\n\n${descriptor.displayName} data was omitted per your choice.`,
541
+ text: `${buildSkippedTag({
542
+ provider: parsed.provider,
543
+ reason: "credential_not_provided",
544
+ remediation,
545
+ silenced,
546
+ })}\n\n${descriptor.displayName} data was omitted per your choice.`,
448
547
  },
449
548
  ],
450
549
  };
@@ -454,16 +553,43 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
454
553
  return undefined;
455
554
  });
456
555
 
556
+ pi.on("tool_call", async (event) => {
557
+ if (!currentRouteToolContext) return undefined;
558
+ const allowed = new Set(currentRouteToolContext.activeToolNames);
559
+ if (allowed.has(event.toolName)) return undefined;
560
+
561
+ const diagnostic = {
562
+ routeKind: currentRouteToolContext.routeKind,
563
+ workflow: currentRouteToolContext.workflow,
564
+ toolName: event.toolName,
565
+ toolBundles: currentRouteToolContext.toolBundles,
566
+ activeToolNames: currentRouteToolContext.activeToolNames,
567
+ };
568
+ pi.appendEntry("opencandle-tool-scope-violation", diagnostic);
569
+
570
+ if (getConfig().toolScopeMode === "enforce") {
571
+ return {
572
+ block: true,
573
+ reason: `Tool ${event.toolName} is outside the route-selected OpenCandle tool bundle.`,
574
+ };
575
+ }
576
+ return undefined;
577
+ });
578
+
457
579
  // Input handling — branches on OPENCANDLE_ROUTER_MODE.
458
580
  pi.on("input", async (event, ctx) => {
459
581
  if (event.source === "extension") return;
582
+ coordinator.clearTickerValidationCache();
460
583
 
461
584
  // Check for comprehensive analysis pattern — same in both modes.
462
585
  const analysis = isAnalysisRequest(event.text);
463
586
  if (analysis.match && analysis.symbol) {
464
- const definition = buildComprehensiveAnalysisDefinition(analysis.symbol, { debate: getConfig().debate });
465
- coordinator.executeWorkflow(pi, definition, ctx);
466
- return { action: "handled" };
587
+ const definition = buildComprehensiveAnalysisDefinition(analysis.symbol, {
588
+ debate: getConfig().debate,
589
+ });
590
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
591
+ if (prompt) markOriginalInput(event.text);
592
+ return prompt ? { action: "transform", text: prompt } : { action: "handled" };
467
593
  }
468
594
 
469
595
  const mode = getConfig().routerMode;
@@ -473,51 +599,221 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
473
599
  // the workflow's queued prompts; tell Pi not to also forward it.
474
600
  // Fallback path (no dispatch) → let Pi pass the user turn through to the
475
601
  // main agent, which will run under the router-supplied fallback context.
476
- return dispatched ? { action: "handled" } : undefined;
602
+ return dispatched || undefined;
477
603
  }
478
604
 
479
- // --- rules mode (default) ---
605
+ // --- explicit legacy rules mode (`OPENCANDLE_ROUTER_MODE=rules`) ---
480
606
  // Extract and persist user preferences (legacy regex path)
481
607
  coordinator.extractAndStorePreferences(event.text);
482
608
  const storage = coordinator.getStorage();
483
609
  const workflowPrefs = storage?.getWorkflowPreferences("global") ?? {};
484
610
 
485
611
  // Classify intent
486
- const classification = classifyIntent(event.text);
612
+ let classification = classifyWithLegacyRules(event.text);
613
+ const ruleModeDisambiguation = disambiguateRulesModeSymbols(
614
+ event.text,
615
+ classification.entities.symbols,
616
+ );
617
+ appendSymbolDropEntries(ruleModeDisambiguation.dropped, "rules");
618
+ classification = {
619
+ ...classification,
620
+ entities: {
621
+ ...classification.entities,
622
+ symbols: ruleModeDisambiguation.kept,
623
+ },
624
+ };
625
+ if (
626
+ isComparePrompt(event.text) &&
627
+ ruleModeDisambiguation.dropped.length > 0 &&
628
+ classification.entities.symbols.length < 2
629
+ ) {
630
+ pi.appendEntry("opencandle-workflow-aborted", {
631
+ reason: "symbol-disambiguation-insufficient-symbols",
632
+ dropped: ruleModeDisambiguation.dropped,
633
+ validSymbols: classification.entities.symbols,
634
+ });
635
+ const base = coordinator.buildRouterContextBase(ctx.sessionManager);
636
+ const output: RouterOutput = {
637
+ routeKind: "clarification",
638
+ route: "fallback",
639
+ workflow: "compare_assets",
640
+ entities: classification.entities,
641
+ slots: {},
642
+ preference_updates: [],
643
+ missing_required: ["symbols"],
644
+ tool_bundles: ["clarification"],
645
+ diagnostics: ruleModeDisambiguation.dropped.map((drop) => ({
646
+ code: "symbol_dropped",
647
+ message: `${drop.token} dropped: ${drop.reason}`,
648
+ details: {
649
+ token: drop.token,
650
+ reason: drop.reason,
651
+ signalsChecked: drop.signalsChecked,
652
+ source: "rules",
653
+ },
654
+ })),
655
+ reasoning: "rules-mode acronym disambiguation left fewer than two symbols for comparison",
656
+ };
657
+ const resolvedTurnContext = buildResolvedTurnContext({ text: event.text, ...base }, output, {
658
+ availableToolNames: safeGetAllToolNames(),
659
+ planning: {
660
+ migrationStatuses: getConfig().planningMigrationStatuses,
661
+ },
662
+ });
663
+ coordinator.setPendingResolvedTurnContext({
664
+ ...resolvedTurnContext,
665
+ diagnostics: [
666
+ ...resolvedTurnContext.diagnostics,
667
+ {
668
+ code: "compare_workflow_aborted",
669
+ message:
670
+ "compare workflow needs at least two validated symbols after acronym disambiguation",
671
+ },
672
+ ],
673
+ });
674
+ coordinator.setPendingFallbackContext({
675
+ assumptionsBlock: [
676
+ "Assumptions Context:",
677
+ classification.entities.symbols.length > 0
678
+ ? ` valid symbols: ${classification.entities.symbols.join(", ")} (user)`
679
+ : " valid symbols: (none)",
680
+ ` dropped ambiguous ticker-like tokens: ${ruleModeDisambiguation.dropped.map((d) => d.token).join(", ")} (no positive ticker signal)`,
681
+ ].join("\n"),
682
+ missingRequired: ["symbols"],
683
+ extraContext:
684
+ "Dropped ambiguous ticker-like tokens: " +
685
+ `${ruleModeDisambiguation.dropped.map((d) => d.token).join(", ")}. ` +
686
+ "Ask the user which ticker symbols they want compared before calling comparison tools.",
687
+ });
688
+ applyRouteToolScope(resolvedTurnContext);
689
+ return undefined;
690
+ }
487
691
 
488
692
  if (classification.workflow === "portfolio_builder") {
489
693
  const resolution = resolvePortfolioSlots(classification.entities, workflowPrefs);
490
- coordinator.recordWorkflowRun("portfolio_builder", classification.entities, resolution.resolved, resolution.defaultsUsed);
491
- pi.appendEntry("opencandle-workflow", { workflow: "portfolio_builder", entities: classification.entities, resolved: resolution.resolved });
694
+ coordinator.recordWorkflowRun(
695
+ "portfolio_builder",
696
+ classification.entities,
697
+ resolution.resolved,
698
+ resolution.defaultsUsed,
699
+ );
700
+ pi.appendEntry("opencandle-workflow", {
701
+ workflow: "portfolio_builder",
702
+ entities: classification.entities,
703
+ resolved: resolution.resolved,
704
+ });
492
705
  const definition = buildPortfolioWorkflowDefinition(resolution);
493
- coordinator.executeWorkflow(pi, definition, ctx);
494
- return { action: "handled" };
706
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
707
+ if (prompt) markOriginalInput(event.text);
708
+ return prompt ? { action: "transform", text: prompt } : { action: "handled" };
495
709
  }
496
710
 
497
711
  if (classification.workflow === "options_screener") {
498
712
  const resolution = resolveOptionsScreenerSlots(classification.entities, workflowPrefs);
499
713
  if (resolution.missingRequired.length === 0) {
500
- coordinator.recordWorkflowRun("options_screener", classification.entities, resolution.resolved, resolution.defaultsUsed);
501
- pi.appendEntry("opencandle-workflow", { workflow: "options_screener", entities: classification.entities, resolved: resolution.resolved });
714
+ coordinator.recordWorkflowRun(
715
+ "options_screener",
716
+ classification.entities,
717
+ resolution.resolved,
718
+ resolution.defaultsUsed,
719
+ );
720
+ pi.appendEntry("opencandle-workflow", {
721
+ workflow: "options_screener",
722
+ entities: classification.entities,
723
+ resolved: resolution.resolved,
724
+ });
502
725
  const definition = buildOptionsScreenerWorkflowDefinition(resolution);
503
- coordinator.executeWorkflow(pi, definition, ctx);
504
- return { action: "handled" };
726
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
727
+ if (prompt) markOriginalInput(event.text);
728
+ return prompt ? { action: "transform", text: prompt } : { action: "handled" };
505
729
  }
506
730
  }
507
731
 
508
- if (classification.workflow === "compare_assets" && classification.entities.symbols.length >= 2) {
732
+ if (
733
+ classification.workflow === "compare_assets" &&
734
+ classification.entities.symbols.length >= 2
735
+ ) {
509
736
  const resolution: SlotResolution<CompareAssetsSlots> = {
510
- resolved: { symbols: classification.entities.symbols },
511
- sources: { symbols: "user" },
737
+ resolved: {
738
+ symbols: classification.entities.symbols,
739
+ metrics: classification.entities.compareMetrics,
740
+ timeHorizon: classification.entities.timeHorizon,
741
+ budget: classification.entities.budget,
742
+ assetScope: classification.entities.assetScope,
743
+ },
744
+ sources: {
745
+ symbols: "user",
746
+ ...(classification.entities.timeHorizon ? { timeHorizon: "user" as const } : {}),
747
+ ...(classification.entities.compareMetrics ? { metrics: "user" as const } : {}),
748
+ ...(classification.entities.budget !== undefined ? { budget: "user" as const } : {}),
749
+ ...(classification.entities.assetScope ? { assetScope: "user" as const } : {}),
750
+ },
512
751
  defaultsUsed: [],
513
752
  missingRequired: [],
514
753
  };
515
- coordinator.recordWorkflowRun("compare_assets", classification.entities, resolution.resolved, resolution.defaultsUsed);
516
- pi.appendEntry("opencandle-workflow", { workflow: "compare_assets", symbols: classification.entities.symbols });
517
- const definition = buildCompareAssetsWorkflowDefinition(resolution);
518
- coordinator.executeWorkflow(pi, definition, ctx);
519
- return { action: "handled" };
754
+ coordinator.recordWorkflowRun(
755
+ "compare_assets",
756
+ classification.entities,
757
+ resolution.resolved,
758
+ resolution.defaultsUsed,
759
+ );
760
+ pi.appendEntry("opencandle-workflow", {
761
+ workflow: "compare_assets",
762
+ symbols: classification.entities.symbols,
763
+ });
764
+ const preflight = await preflightCompareResolution(resolution);
765
+ if (!preflight) {
766
+ coordinator.recordWorkflowRun(
767
+ "fallback",
768
+ classification.entities,
769
+ resolution.resolved,
770
+ [],
771
+ "clarification",
772
+ );
773
+ coordinator.setPendingFallbackContext({
774
+ assumptionsBlock: [
775
+ "Assumptions Context:",
776
+ ` original symbols: ${classification.entities.symbols.join(", ")} (user)`,
777
+ ].join("\n"),
778
+ missingRequired: ["symbols"],
779
+ extraContext:
780
+ "Compare workflow aborted because ticker preflight left fewer than two valid symbols. Ask the user to clarify the intended tickers before calling comparison tools.",
781
+ });
782
+ return undefined;
783
+ }
784
+ const definition = buildCompareAssetsWorkflowDefinition(preflight.resolution);
785
+ applyPreflightAnnotation(definition, preflight.dropped);
786
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
787
+ if (prompt) markOriginalInput(event.text);
788
+ return prompt ? { action: "transform", text: prompt } : { action: "handled" };
789
+ }
790
+
791
+ // Rules-mode finance fallback: no workflow dispatched, but the turn is
792
+ // finance-shaped (classified finance intent, extracted symbols, or finance
793
+ // vocabulary). Record the fallback turn and stash a fallback context so
794
+ // the system prompt carries saved market state for this turn; non-finance
795
+ // prompts stay untouched.
796
+ const isFinanceFallback =
797
+ classification.workflow !== "unclassified" ||
798
+ classification.entities.symbols.length > 0 ||
799
+ hasFinanceSignals(event.text);
800
+ if (isFinanceFallback) {
801
+ coordinator.recordWorkflowRun("fallback", classification.entities, {}, [], "agent_task");
802
+ coordinator.setPendingFallbackContext({
803
+ assumptionsBlock: "",
804
+ missingRequired: [],
805
+ extraContext:
806
+ classification.entities.symbols.length > 0
807
+ ? `Rules-router extracted symbols: ${classification.entities.symbols.join(", ")}.`
808
+ : undefined,
809
+ });
810
+ pi.appendEntry("opencandle-fallback-context", {
811
+ mode: "rules",
812
+ classifiedWorkflow: classification.workflow,
813
+ symbols: classification.entities.symbols,
814
+ });
520
815
  }
816
+ return undefined;
521
817
  });
522
818
 
523
819
  /**
@@ -530,10 +826,11 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
530
826
  async function handleLlmRouterTurn(
531
827
  text: string,
532
828
  ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
533
- ): Promise<boolean> {
829
+ ): Promise<{ action: "transform"; text: string } | false> {
534
830
  const storage = coordinator.getStorage();
535
- const { profileSnapshot, recentWorkflowRuns, priorTurns } =
536
- coordinator.buildRouterContextBase(ctx.sessionManager);
831
+ const { profileSnapshot, recentWorkflowRuns, priorTurns } = coordinator.buildRouterContextBase(
832
+ ctx.sessionManager,
833
+ );
537
834
  // priorTurns is not scrubbed for /forget — tracked in proposal.md follow-ups.
538
835
  const input: RouterInputContext = {
539
836
  text,
@@ -563,7 +860,34 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
563
860
  return false;
564
861
  }
565
862
 
863
+ const availableToolNames = safeGetAllToolNames();
864
+ const memory = coordinator.retrieveMemoryForRoute(
865
+ output.routeKind,
866
+ output.workflow,
867
+ Object.keys(output.slots),
868
+ );
869
+ const resolvedTurnContext = buildResolvedTurnContext(input, output, {
870
+ availableToolNames,
871
+ memoryEntries: memory.entries,
872
+ filteredMemory: memory.filtered.map(({ entry, reason }) => ({
873
+ category: entry.category,
874
+ key: entry.key,
875
+ source: entry.source,
876
+ recordedAt: entry.recordedAt,
877
+ confidence: entry.confidence,
878
+ filtered: true,
879
+ filterReason: reason,
880
+ })),
881
+ planning: {
882
+ migrationStatuses: getConfig().planningMigrationStatuses,
883
+ },
884
+ });
885
+
566
886
  pi.appendEntry("opencandle-router", { output });
887
+ appendRouterSymbolDropEntries(output);
888
+ pi.appendEntry("opencandle-route-context", resolvedTurnContext);
889
+ coordinator.setPendingResolvedTurnContext(resolvedTurnContext);
890
+ applyRouteToolScope(resolvedTurnContext);
567
891
 
568
892
  // Preference writes: HIGH-confidence only. Medium/low are logged for
569
893
  // observability even when no storage is available.
@@ -585,8 +909,12 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
585
909
  }
586
910
 
587
911
  // Workflow dispatch for recognised workflows.
588
- if (output.route === "workflow" && output.workflow) {
589
- return dispatchRouterWorkflow(output, ctx);
912
+ if (output.routeKind === "workflow_dispatch" && output.workflow) {
913
+ return dispatchRouterWorkflow(output, ctx, text);
914
+ }
915
+
916
+ if (output.routeKind === "pass_through") {
917
+ return false;
590
918
  }
591
919
 
592
920
  // Fallback: record the turn and stash the fallback context for the
@@ -596,90 +924,136 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
596
924
  output.entities,
597
925
  Object.fromEntries(Object.entries(output.slots).map(([k, v]) => [k, v.value])),
598
926
  [],
599
- "fallback",
927
+ output.routeKind,
600
928
  );
601
929
 
602
930
  const assumptionsBlock = buildAssumptionsBlockFromRouter(output.slots);
603
931
  coordinator.setPendingFallbackContext({
604
932
  assumptionsBlock,
605
933
  missingRequired: output.missing_required,
606
- extraContext: output.entities.symbols.length > 0
607
- ? `Router-extracted symbols: ${output.entities.symbols.join(", ")}.`
608
- : undefined,
934
+ extraContext:
935
+ output.entities.symbols.length > 0
936
+ ? `Router-extracted symbols: ${output.entities.symbols.join(", ")}.` +
937
+ ` Route kind: ${output.routeKind}. Tool bundles: ${output.tool_bundles.join(", ") || "(none)"}.`
938
+ : undefined,
609
939
  });
610
940
  return false;
611
941
  }
612
942
 
613
- function dispatchRouterWorkflow(
943
+ async function dispatchRouterWorkflow(
614
944
  output: RouterOutput,
615
945
  ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
616
- ): boolean {
946
+ originalText: string,
947
+ ): Promise<{ action: "transform"; text: string } | false> {
617
948
  const workflow = output.workflow!;
618
949
  const storage = coordinator.getStorage();
619
950
  const workflowPrefs = storage?.getWorkflowPreferences("global") ?? {};
951
+ const entities = mergeRouterSlotsIntoEntities(output);
620
952
 
621
953
  if (workflow === "portfolio_builder") {
622
- const resolution = resolvePortfolioSlots(output.entities, workflowPrefs);
954
+ const resolution = withRouterSlotSources(
955
+ resolvePortfolioSlots(entities, workflowPrefs),
956
+ output,
957
+ );
623
958
  coordinator.recordWorkflowRun(
624
959
  "portfolio_builder",
625
- output.entities,
960
+ entities,
626
961
  resolution.resolved,
627
962
  resolution.defaultsUsed,
628
- "workflow",
963
+ output.routeKind,
629
964
  );
630
965
  pi.appendEntry("opencandle-workflow", {
631
966
  workflow: "portfolio_builder",
632
- entities: output.entities,
967
+ entities,
633
968
  resolved: resolution.resolved,
634
969
  });
635
970
  const definition = buildPortfolioWorkflowDefinition(resolution);
636
- coordinator.executeWorkflow(pi, definition, ctx);
637
- return true;
971
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
972
+ if (prompt) markOriginalInput(originalText);
973
+ return prompt ? { action: "transform", text: prompt } : false;
638
974
  }
639
975
  if (workflow === "options_screener") {
640
- const resolution = resolveOptionsScreenerSlots(output.entities, workflowPrefs);
976
+ const resolution = withRouterSlotSources(
977
+ resolveOptionsScreenerSlots(entities, workflowPrefs),
978
+ output,
979
+ );
641
980
  // Router may emit missing_required; main agent handles via ask_user.
642
981
  // Still dispatch the workflow when symbol is present.
643
982
  if (resolution.missingRequired.length === 0) {
644
983
  coordinator.recordWorkflowRun(
645
984
  "options_screener",
646
- output.entities,
985
+ entities,
647
986
  resolution.resolved,
648
987
  resolution.defaultsUsed,
649
- "workflow",
988
+ output.routeKind,
650
989
  );
651
990
  pi.appendEntry("opencandle-workflow", {
652
991
  workflow: "options_screener",
653
- entities: output.entities,
992
+ entities,
654
993
  resolved: resolution.resolved,
655
994
  });
656
995
  const definition = buildOptionsScreenerWorkflowDefinition(resolution);
657
- coordinator.executeWorkflow(pi, definition, ctx);
658
- return true;
996
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
997
+ if (prompt) markOriginalInput(originalText);
998
+ return prompt ? { action: "transform", text: prompt } : false;
659
999
  }
660
1000
  // Missing required symbol — treat as fallback with ask_user directive.
661
1001
  }
662
- if (workflow === "compare_assets" && output.entities.symbols.length >= 2) {
1002
+ if (workflow === "compare_assets" && entities.symbols.length >= 2) {
663
1003
  const resolution: SlotResolution<CompareAssetsSlots> = {
664
- resolved: { symbols: output.entities.symbols },
665
- sources: { symbols: "user" },
1004
+ resolved: {
1005
+ symbols: entities.symbols,
1006
+ metrics: entities.compareMetrics,
1007
+ timeHorizon: entities.timeHorizon,
1008
+ budget: entities.budget,
1009
+ assetScope: entities.assetScope,
1010
+ },
1011
+ sources: {
1012
+ symbols: sourceForRouterSlot(output, "symbols", "user"),
1013
+ ...(entities.timeHorizon ? { timeHorizon: "user" as const } : {}),
1014
+ ...(entities.compareMetrics ? { metrics: "user" as const } : {}),
1015
+ ...(entities.budget !== undefined
1016
+ ? { budget: sourceForRouterSlot(output, "budget", "user") }
1017
+ : {}),
1018
+ ...(entities.assetScope ? { assetScope: "user" as const } : {}),
1019
+ },
666
1020
  defaultsUsed: [],
667
1021
  missingRequired: [],
668
1022
  };
669
1023
  coordinator.recordWorkflowRun(
670
1024
  "compare_assets",
671
- output.entities,
1025
+ entities,
672
1026
  resolution.resolved,
673
1027
  [],
674
- "workflow",
1028
+ output.routeKind,
675
1029
  );
676
1030
  pi.appendEntry("opencandle-workflow", {
677
1031
  workflow: "compare_assets",
678
- symbols: output.entities.symbols,
1032
+ symbols: entities.symbols,
679
1033
  });
680
- const definition = buildCompareAssetsWorkflowDefinition(resolution);
681
- coordinator.executeWorkflow(pi, definition, ctx);
682
- return true;
1034
+ const preflight = await preflightCompareResolution(resolution);
1035
+ if (!preflight) {
1036
+ coordinator.recordWorkflowRun(
1037
+ "fallback",
1038
+ output.entities,
1039
+ Object.fromEntries(Object.entries(output.slots).map(([k, v]) => [k, v.value])),
1040
+ [],
1041
+ output.routeKind,
1042
+ );
1043
+ coordinator.setPendingResolvedTurnContext(null);
1044
+ coordinator.setPendingFallbackContext({
1045
+ assumptionsBlock: buildAssumptionsBlockFromRouter(output.slots),
1046
+ missingRequired: ["symbols"],
1047
+ extraContext:
1048
+ "Compare workflow aborted because ticker preflight left fewer than two valid symbols. Ask the user to clarify the intended tickers.",
1049
+ });
1050
+ return false;
1051
+ }
1052
+ const definition = buildCompareAssetsWorkflowDefinition(preflight.resolution);
1053
+ applyPreflightAnnotation(definition, preflight.dropped);
1054
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
1055
+ if (prompt) markOriginalInput(originalText);
1056
+ return prompt ? { action: "transform", text: prompt } : false;
683
1057
  }
684
1058
 
685
1059
  // single_asset_analysis / watchlist / general_qa + any workflow with
@@ -690,17 +1064,276 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
690
1064
  output.entities,
691
1065
  Object.fromEntries(Object.entries(output.slots).map(([k, v]) => [k, v.value])),
692
1066
  [],
693
- "fallback",
1067
+ output.routeKind,
694
1068
  );
695
1069
  const assumptionsBlock = buildAssumptionsBlockFromRouter(output.slots);
696
1070
  coordinator.setPendingFallbackContext({
697
1071
  assumptionsBlock,
698
1072
  missingRequired: output.missing_required,
699
- extraContext: `Router classified as ${workflow} but declined to dispatch. Symbols: ${output.entities.symbols.join(", ") || "(none)"}.`,
1073
+ extraContext: `Router classified as ${workflow} but declined to dispatch. Symbols: ${entities.symbols.join(", ") || "(none)"}.`,
700
1074
  });
701
1075
  return false;
702
1076
  }
703
1077
 
1078
+ function appendRouterSymbolDropEntries(output: RouterOutput): void {
1079
+ for (const diagnostic of output.diagnostics) {
1080
+ if (diagnostic.code !== "symbol_dropped") continue;
1081
+ const details = diagnostic.details ?? {};
1082
+ appendSymbolDropEntries(
1083
+ [
1084
+ {
1085
+ token: String(details.token ?? ""),
1086
+ reason: String(details.reason ?? ""),
1087
+ signalsChecked: Array.isArray(details.signalsChecked)
1088
+ ? details.signalsChecked.map(String)
1089
+ : [],
1090
+ },
1091
+ ],
1092
+ String(details.source ?? "llm"),
1093
+ );
1094
+ }
1095
+ }
1096
+
1097
+ function appendSymbolDropEntries(
1098
+ dropped: Array<{ token: string; reason: string; signalsChecked: string[] }>,
1099
+ source: string,
1100
+ ): void {
1101
+ for (const drop of dropped) {
1102
+ pi.appendEntry("opencandle-symbol-dropped", {
1103
+ token: drop.token,
1104
+ reason: drop.reason,
1105
+ signalsChecked: drop.signalsChecked,
1106
+ source,
1107
+ });
1108
+ }
1109
+ }
1110
+
1111
+ function disambiguateRulesModeSymbols(
1112
+ text: string,
1113
+ extractedSymbols: string[],
1114
+ ): {
1115
+ kept: string[];
1116
+ dropped: Array<{ token: string; reason: string; signalsChecked: string[] }>;
1117
+ } {
1118
+ const candidates = mergeSymbols(extractedSymbols, rawTickerLikeTokens(text));
1119
+ const disambiguated = disambiguateSymbols(candidates, text);
1120
+ return {
1121
+ kept: disambiguated.kept.filter((symbol) => extractedSymbols.includes(symbol)),
1122
+ dropped: disambiguated.dropped,
1123
+ };
1124
+ }
1125
+
1126
+ function rawTickerLikeTokens(text: string): string[] {
1127
+ const tokens: string[] = [];
1128
+ for (const match of text.matchAll(/\$?([A-Za-z]{1,5})\b/g)) {
1129
+ const raw = match[1];
1130
+ if (raw !== raw.toUpperCase()) continue;
1131
+ const token = raw.toUpperCase();
1132
+ if (!tokens.includes(token)) tokens.push(token);
1133
+ }
1134
+ return tokens;
1135
+ }
1136
+
1137
+ function isComparePrompt(text: string): boolean {
1138
+ return /\b(?:compare|vs\.?|versus|which\s+is\s+better)\b/i.test(text);
1139
+ }
1140
+
1141
+ async function preflightCompareResolution(
1142
+ resolution: SlotResolution<CompareAssetsSlots>,
1143
+ ): Promise<{
1144
+ resolution: SlotResolution<CompareAssetsSlots>;
1145
+ dropped: Array<{ symbol: string; reason: string }>;
1146
+ } | null> {
1147
+ const result = await preflightSymbols(resolution.resolved.symbols, {
1148
+ cache: coordinator.getTickerValidationCache(),
1149
+ search: options?.symbolSearch,
1150
+ });
1151
+ for (const drop of result.dropped) {
1152
+ pi.appendEntry("opencandle-symbol-preflight-dropped", drop);
1153
+ }
1154
+ if (result.valid.length < 2) {
1155
+ pi.appendEntry("opencandle-workflow-aborted", {
1156
+ reason: "preflight-insufficient-symbols",
1157
+ dropped: result.dropped,
1158
+ });
1159
+ return null;
1160
+ }
1161
+ return {
1162
+ resolution: {
1163
+ ...resolution,
1164
+ resolved: {
1165
+ ...resolution.resolved,
1166
+ symbols: result.valid,
1167
+ },
1168
+ },
1169
+ dropped: result.dropped,
1170
+ };
1171
+ }
1172
+
1173
+ function applyPreflightAnnotation(
1174
+ definition: ReturnType<typeof buildCompareAssetsWorkflowDefinition>,
1175
+ dropped: Array<{ symbol: string; reason: string }>,
1176
+ ): void {
1177
+ if (dropped.length === 0 || definition.steps.length === 0) return;
1178
+ definition.steps[0] = {
1179
+ ...definition.steps[0],
1180
+ prompt: `${formatPreflightDropAnnotation(dropped)}\n\n${definition.steps[0].prompt}`,
1181
+ };
1182
+ }
1183
+
1184
+ function mergeRouterSlotsIntoEntities(output: RouterOutput): ExtractedEntities {
1185
+ const entities: ExtractedEntities = {
1186
+ ...output.entities,
1187
+ symbols: output.entities.symbols,
1188
+ };
1189
+
1190
+ if (entities.budget === undefined && typeof output.slots.budget?.value === "number") {
1191
+ entities.budget = output.slots.budget.value;
1192
+ }
1193
+
1194
+ const droppedSymbols = droppedSymbolsFromDiagnostics(output);
1195
+ const slotSymbols = symbolsFromRouterSlots(output).filter(
1196
+ (symbol) => !droppedSymbols.has(symbol),
1197
+ );
1198
+ if (slotSymbols.length > 0 && slotSymbols.length > entities.symbols.length) {
1199
+ entities.symbols = mergeSymbols(slotSymbols, entities.symbols);
1200
+ }
1201
+
1202
+ return entities;
1203
+ }
1204
+
1205
+ function droppedSymbolsFromDiagnostics(output: RouterOutput): Set<string> {
1206
+ const dropped = new Set<string>();
1207
+ for (const diagnostic of output.diagnostics) {
1208
+ if (diagnostic.code !== "symbol_dropped") continue;
1209
+ const token = diagnostic.details?.token;
1210
+ if (typeof token === "string" && token.trim() !== "") {
1211
+ dropped.add(token.toUpperCase());
1212
+ }
1213
+ }
1214
+ return dropped;
1215
+ }
1216
+
1217
+ function withRouterSlotSources<T extends object>(
1218
+ resolution: SlotResolution<T>,
1219
+ output: RouterOutput,
1220
+ ): SlotResolution<T> {
1221
+ const sources: Record<string, SlotSource | undefined> = { ...resolution.sources };
1222
+ if (output.entities.budget === undefined && output.slots.budget) {
1223
+ sources.budget = output.slots.budget.source;
1224
+ }
1225
+ if (output.entities.symbols.length === 0 && output.slots.symbol) {
1226
+ sources.symbol = output.slots.symbol.source;
1227
+ }
1228
+ if (output.entities.symbols.length < 2 && output.slots.symbols) {
1229
+ sources.symbols = output.slots.symbols.source;
1230
+ }
1231
+ return { ...resolution, sources: sources as SlotResolution<T>["sources"] };
1232
+ }
1233
+
1234
+ function sourceForRouterSlot(
1235
+ output: RouterOutput,
1236
+ slotName: "symbol" | "symbols" | "budget",
1237
+ fallback: SlotSource,
1238
+ ): SlotSource {
1239
+ return output.slots[slotName]?.source ?? fallback;
1240
+ }
1241
+
1242
+ function symbolsFromRouterSlots(output: RouterOutput): string[] {
1243
+ const symbols: string[] = [];
1244
+ const symbol = output.slots.symbol?.value;
1245
+ if (typeof symbol === "string" && symbol.trim() !== "") {
1246
+ symbols.push(symbol.toUpperCase());
1247
+ }
1248
+ const symbolList = output.slots.symbols?.value;
1249
+ if (Array.isArray(symbolList)) {
1250
+ for (const value of symbolList) {
1251
+ if (typeof value === "string" && value.trim() !== "") {
1252
+ symbols.push(value.toUpperCase());
1253
+ }
1254
+ }
1255
+ }
1256
+ return symbols;
1257
+ }
1258
+
1259
+ function mergeSymbols(primary: string[], secondary: string[]): string[] {
1260
+ const merged: string[] = [];
1261
+ for (const symbol of [...primary, ...secondary]) {
1262
+ if (!merged.includes(symbol)) merged.push(symbol);
1263
+ }
1264
+ return merged;
1265
+ }
1266
+
1267
+ function safeGetAllToolNames(): string[] {
1268
+ try {
1269
+ return pi.getAllTools().map((tool) => tool.name);
1270
+ } catch {
1271
+ return [];
1272
+ }
1273
+ }
1274
+
1275
+ function applyRouteToolScope(context: ResolvedTurnContext): void {
1276
+ const mode = getConfig().toolScopeMode;
1277
+ currentRouteToolContext = context;
1278
+ pi.appendEntry("opencandle-tool-scope", {
1279
+ mode,
1280
+ routeKind: context.routeKind,
1281
+ workflow: context.workflow,
1282
+ toolBundles: context.toolBundles,
1283
+ activeToolNames: context.activeToolNames,
1284
+ enforced: false,
1285
+ });
1286
+
1287
+ if (mode !== "enforce") return;
1288
+ if (context.activeToolNames.length === 0) return;
1289
+
1290
+ try {
1291
+ if (activeToolSnapshot === null) {
1292
+ activeToolSnapshot = pi.getActiveTools();
1293
+ }
1294
+ pi.setActiveTools(context.activeToolNames);
1295
+ pi.appendEntry("opencandle-tool-scope", {
1296
+ mode,
1297
+ routeKind: context.routeKind,
1298
+ workflow: context.workflow,
1299
+ toolBundles: context.toolBundles,
1300
+ activeToolNames: context.activeToolNames,
1301
+ enforced: true,
1302
+ });
1303
+ } catch (err) {
1304
+ pi.appendEntry("opencandle-tool-scope", {
1305
+ mode,
1306
+ routeKind: context.routeKind,
1307
+ workflow: context.workflow,
1308
+ toolBundles: context.toolBundles,
1309
+ activeToolNames: context.activeToolNames,
1310
+ enforced: false,
1311
+ diagnostic: err instanceof Error ? err.message : String(err),
1312
+ });
1313
+ }
1314
+ }
1315
+
1316
+ function restoreRouteToolScope(): void {
1317
+ currentRouteToolContext = null;
1318
+ if (activeToolSnapshot === null) return;
1319
+ try {
1320
+ pi.setActiveTools(activeToolSnapshot);
1321
+ pi.appendEntry("opencandle-tool-scope", {
1322
+ mode: getConfig().toolScopeMode,
1323
+ restored: true,
1324
+ activeToolNames: activeToolSnapshot,
1325
+ });
1326
+ } catch (err) {
1327
+ pi.appendEntry("opencandle-tool-scope", {
1328
+ mode: getConfig().toolScopeMode,
1329
+ restored: false,
1330
+ diagnostic: err instanceof Error ? err.message : String(err),
1331
+ });
1332
+ } finally {
1333
+ activeToolSnapshot = null;
1334
+ }
1335
+ }
1336
+
704
1337
  function resolveRouterLlmClient(
705
1338
  ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
706
1339
  ): RouterLlmClient | null {
@@ -717,8 +1350,50 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
717
1350
  // is pending (router-mode fallback turns), inject it into the prompt.
718
1351
  pi.on("before_agent_start", async (event) => {
719
1352
  const fallbackContext = coordinator.consumePendingFallbackContext() ?? undefined;
1353
+ const resolvedTurnContext = coordinator.consumePendingResolvedTurnContext() ?? undefined;
720
1354
  return {
721
- systemPrompt: coordinator.buildSystemPrompt(event.systemPrompt, undefined, fallbackContext),
1355
+ systemPrompt: coordinator.buildSystemPrompt(
1356
+ event.systemPrompt,
1357
+ coordinator.getActiveWorkflowType(),
1358
+ fallbackContext,
1359
+ resolvedTurnContext,
1360
+ ),
722
1361
  };
723
1362
  });
724
1363
  }
1364
+
1365
+ /** Concatenate text from a Pi message `content` (plain string or block array). */
1366
+ function extractMessageText(content: unknown): string {
1367
+ if (typeof content === "string") return content;
1368
+ if (!Array.isArray(content)) return "";
1369
+ let text = "";
1370
+ for (const block of content) {
1371
+ if (
1372
+ block &&
1373
+ typeof block === "object" &&
1374
+ (block as { type?: unknown }).type === "text" &&
1375
+ typeof (block as { text?: unknown }).text === "string"
1376
+ ) {
1377
+ text += (block as { text: string }).text;
1378
+ }
1379
+ }
1380
+ return text;
1381
+ }
1382
+
1383
+ /**
1384
+ * True when the session name is still an auto-set placeholder that the LLM
1385
+ * title may replace: unset, exactly one of the candidate user texts, or the
1386
+ * GUI server's truncated "first 77 chars + ..." form of one of them.
1387
+ */
1388
+ function isPlaceholderSessionName(name: string | undefined, candidates: string[]): boolean {
1389
+ if (name === undefined || name.trim().length === 0) return true;
1390
+ const trimmed = name.trim();
1391
+ for (const candidate of candidates) {
1392
+ const candidateTrimmed = candidate.trim();
1393
+ if (trimmed === candidateTrimmed) return true;
1394
+ if (trimmed.endsWith("...") && candidateTrimmed.startsWith(trimmed.slice(0, -3))) {
1395
+ return true;
1396
+ }
1397
+ }
1398
+ return false;
1399
+ }