opencandle 0.4.0 → 0.5.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 (251) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -14
  3. package/dist/cli.js +2 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +19 -3
  6. package/dist/config.js +61 -2
  7. package/dist/config.js.map +1 -1
  8. package/dist/infra/browser.d.ts +1 -3
  9. package/dist/infra/browser.js +1 -1
  10. package/dist/infra/browser.js.map +1 -1
  11. package/dist/infra/rate-limiter.d.ts +4 -0
  12. package/dist/infra/rate-limiter.js +5 -1
  13. package/dist/infra/rate-limiter.js.map +1 -1
  14. package/dist/memory/manager.d.ts +9 -0
  15. package/dist/memory/manager.js +28 -11
  16. package/dist/memory/manager.js.map +1 -1
  17. package/dist/memory/storage.d.ts +3 -2
  18. package/dist/memory/storage.js.map +1 -1
  19. package/dist/memory/types.js +4 -0
  20. package/dist/memory/types.js.map +1 -1
  21. package/dist/pi/opencandle-extension.js +230 -36
  22. package/dist/pi/opencandle-extension.js.map +1 -1
  23. package/dist/pi/setup.js +10 -0
  24. package/dist/pi/setup.js.map +1 -1
  25. package/dist/prompts/context-builder.d.ts +18 -3
  26. package/dist/prompts/context-builder.js +102 -16
  27. package/dist/prompts/context-builder.js.map +1 -1
  28. package/dist/prompts/disclaimer.js +1 -1
  29. package/dist/prompts/disclaimer.js.map +1 -1
  30. package/dist/prompts/policy-cards.d.ts +13 -0
  31. package/dist/prompts/policy-cards.js +197 -0
  32. package/dist/prompts/policy-cards.js.map +1 -0
  33. package/dist/prompts/sections.js +3 -3
  34. package/dist/prompts/sections.js.map +1 -1
  35. package/dist/prompts/workflow-prompts.js +170 -18
  36. package/dist/prompts/workflow-prompts.js.map +1 -1
  37. package/dist/providers/alpha-vantage.js +23 -1
  38. package/dist/providers/alpha-vantage.js.map +1 -1
  39. package/dist/providers/sec-edgar.d.ts +8 -1
  40. package/dist/providers/sec-edgar.js +172 -5
  41. package/dist/providers/sec-edgar.js.map +1 -1
  42. package/dist/providers/yahoo-finance.d.ts +2 -0
  43. package/dist/providers/yahoo-finance.js +134 -3
  44. package/dist/providers/yahoo-finance.js.map +1 -1
  45. package/dist/routing/classify-intent.d.ts +3 -0
  46. package/dist/routing/classify-intent.js +82 -3
  47. package/dist/routing/classify-intent.js.map +1 -1
  48. package/dist/routing/defaults.js +3 -3
  49. package/dist/routing/defaults.js.map +1 -1
  50. package/dist/routing/entity-extractor.d.ts +1 -0
  51. package/dist/routing/entity-extractor.js +158 -12
  52. package/dist/routing/entity-extractor.js.map +1 -1
  53. package/dist/routing/index.d.ts +7 -1
  54. package/dist/routing/index.js +4 -0
  55. package/dist/routing/index.js.map +1 -1
  56. package/dist/routing/legacy-rule-router.d.ts +9 -0
  57. package/dist/routing/legacy-rule-router.js +12 -0
  58. package/dist/routing/legacy-rule-router.js.map +1 -0
  59. package/dist/routing/planning.d.ts +54 -0
  60. package/dist/routing/planning.js +531 -0
  61. package/dist/routing/planning.js.map +1 -0
  62. package/dist/routing/route-manifest.d.ts +35 -0
  63. package/dist/routing/route-manifest.js +221 -0
  64. package/dist/routing/route-manifest.js.map +1 -0
  65. package/dist/routing/router-prompt.js +45 -42
  66. package/dist/routing/router-prompt.js.map +1 -1
  67. package/dist/routing/router-types.d.ts +9 -0
  68. package/dist/routing/router.d.ts +1 -0
  69. package/dist/routing/router.js +456 -12
  70. package/dist/routing/router.js.map +1 -1
  71. package/dist/routing/slot-resolver.js +46 -6
  72. package/dist/routing/slot-resolver.js.map +1 -1
  73. package/dist/routing/turn-context.d.ts +44 -0
  74. package/dist/routing/turn-context.js +45 -0
  75. package/dist/routing/turn-context.js.map +1 -0
  76. package/dist/routing/types.d.ts +13 -1
  77. package/dist/runtime/answer-contracts.d.ts +82 -0
  78. package/dist/runtime/answer-contracts.js +414 -0
  79. package/dist/runtime/answer-contracts.js.map +1 -0
  80. package/dist/runtime/artifact-contracts.d.ts +14 -0
  81. package/dist/runtime/artifact-contracts.js +57 -0
  82. package/dist/runtime/artifact-contracts.js.map +1 -0
  83. package/dist/runtime/planning-evidence.d.ts +99 -0
  84. package/dist/runtime/planning-evidence.js +445 -0
  85. package/dist/runtime/planning-evidence.js.map +1 -0
  86. package/dist/runtime/session-coordinator.d.ts +20 -2
  87. package/dist/runtime/session-coordinator.js +47 -14
  88. package/dist/runtime/session-coordinator.js.map +1 -1
  89. package/dist/system-prompt.js +4 -1
  90. package/dist/system-prompt.js.map +1 -1
  91. package/dist/tools/fundamentals/company-overview.js +1 -1
  92. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  93. package/dist/tools/fundamentals/comps.js +1 -1
  94. package/dist/tools/fundamentals/comps.js.map +1 -1
  95. package/dist/tools/fundamentals/dcf.js +1 -1
  96. package/dist/tools/fundamentals/dcf.js.map +1 -1
  97. package/dist/tools/fundamentals/earnings.js +1 -1
  98. package/dist/tools/fundamentals/earnings.js.map +1 -1
  99. package/dist/tools/fundamentals/financials.js +1 -1
  100. package/dist/tools/fundamentals/financials.js.map +1 -1
  101. package/dist/tools/fundamentals/sec-filings.d.ts +1 -0
  102. package/dist/tools/fundamentals/sec-filings.js +19 -2
  103. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  104. package/dist/tools/index.d.ts +1 -0
  105. package/dist/tools/index.js +3 -0
  106. package/dist/tools/index.js.map +1 -1
  107. package/dist/tools/macro/fear-greed.js +1 -1
  108. package/dist/tools/macro/fear-greed.js.map +1 -1
  109. package/dist/tools/macro/fred-data.js +29 -5
  110. package/dist/tools/macro/fred-data.js.map +1 -1
  111. package/dist/tools/market/crypto-history.js +18 -2
  112. package/dist/tools/market/crypto-history.js.map +1 -1
  113. package/dist/tools/market/crypto-price.js +1 -1
  114. package/dist/tools/market/crypto-price.js.map +1 -1
  115. package/dist/tools/market/search-ticker.js +1 -1
  116. package/dist/tools/market/search-ticker.js.map +1 -1
  117. package/dist/tools/market/stock-history.js +1 -1
  118. package/dist/tools/market/stock-history.js.map +1 -1
  119. package/dist/tools/market/stock-quote.js +1 -1
  120. package/dist/tools/market/stock-quote.js.map +1 -1
  121. package/dist/tools/options/greeks.js +0 -1
  122. package/dist/tools/options/greeks.js.map +1 -1
  123. package/dist/tools/options/option-chain.js +9 -4
  124. package/dist/tools/options/option-chain.js.map +1 -1
  125. package/dist/tools/portfolio/correlation.js +1 -1
  126. package/dist/tools/portfolio/correlation.js.map +1 -1
  127. package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
  128. package/dist/tools/portfolio/holdings-overlap.js +105 -0
  129. package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
  130. package/dist/tools/portfolio/predictions.js +1 -1
  131. package/dist/tools/portfolio/predictions.js.map +1 -1
  132. package/dist/tools/portfolio/risk-analysis.js +1 -1
  133. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  134. package/dist/tools/portfolio/tracker.js +1 -1
  135. package/dist/tools/portfolio/tracker.js.map +1 -1
  136. package/dist/tools/portfolio/watchlist.js +12 -4
  137. package/dist/tools/portfolio/watchlist.js.map +1 -1
  138. package/dist/tools/sentiment/reddit-sentiment.js +1 -1
  139. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  140. package/dist/tools/sentiment/sentiment-summary.js +57 -2
  141. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  142. package/dist/tools/sentiment/twitter-sentiment.js +1 -1
  143. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  144. package/dist/tools/sentiment/web-search.js +32 -3
  145. package/dist/tools/sentiment/web-search.js.map +1 -1
  146. package/dist/tools/sentiment/web-sentiment.js +1 -1
  147. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  148. package/dist/tools/technical/backtest.d.ts +2 -2
  149. package/dist/tools/technical/backtest.js +41 -27
  150. package/dist/tools/technical/backtest.js.map +1 -1
  151. package/dist/tools/technical/indicators.js +1 -3
  152. package/dist/tools/technical/indicators.js.map +1 -1
  153. package/dist/types/options.d.ts +10 -0
  154. package/dist/types/portfolio.d.ts +27 -0
  155. package/dist/workflows/compare-assets.js +38 -2
  156. package/dist/workflows/compare-assets.js.map +1 -1
  157. package/dist/workflows/options-screener.js +88 -7
  158. package/dist/workflows/options-screener.js.map +1 -1
  159. package/dist/workflows/portfolio-builder.js +7 -3
  160. package/dist/workflows/portfolio-builder.js.map +1 -1
  161. package/gui/server/ask-user-bridge.ts +82 -0
  162. package/gui/server/gui-session-manager.ts +5 -0
  163. package/gui/server/projector.ts +47 -5
  164. package/gui/server/prompt-observation.ts +61 -0
  165. package/gui/server/server.ts +119 -8
  166. package/gui/server/session-entry-wait.ts +81 -0
  167. package/gui/web/dist/assets/{CatalogOverlay-D1ImSJTe.js → CatalogOverlay-Bmp6Knu7.js} +1 -1
  168. package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
  169. package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
  170. package/gui/web/dist/index.html +2 -2
  171. package/package.json +18 -12
  172. package/src/cli.ts +2 -1
  173. package/src/config.ts +89 -5
  174. package/src/infra/browser.ts +1 -1
  175. package/src/infra/rate-limiter.ts +10 -1
  176. package/src/memory/manager.ts +43 -10
  177. package/src/memory/storage.ts +3 -2
  178. package/src/memory/types.ts +4 -0
  179. package/src/pi/opencandle-extension.ts +273 -42
  180. package/src/pi/setup.ts +10 -0
  181. package/src/prompts/context-builder.ts +128 -17
  182. package/src/prompts/disclaimer.ts +1 -1
  183. package/src/prompts/policy-cards.ts +220 -0
  184. package/src/prompts/sections.ts +3 -3
  185. package/src/prompts/workflow-prompts.ts +172 -18
  186. package/src/providers/alpha-vantage.ts +24 -1
  187. package/src/providers/sec-edgar.ts +220 -4
  188. package/src/providers/web-search.ts +1 -1
  189. package/src/providers/yahoo-finance.ts +171 -4
  190. package/src/routing/classify-intent.ts +94 -3
  191. package/src/routing/defaults.ts +3 -3
  192. package/src/routing/entity-extractor.ts +164 -13
  193. package/src/routing/index.ts +44 -0
  194. package/src/routing/legacy-rule-router.ts +13 -0
  195. package/src/routing/planning.ts +732 -0
  196. package/src/routing/route-manifest.ts +287 -0
  197. package/src/routing/router-prompt.ts +50 -46
  198. package/src/routing/router-types.ts +21 -0
  199. package/src/routing/router.ts +511 -12
  200. package/src/routing/slot-resolver.ts +44 -6
  201. package/src/routing/turn-context.ts +111 -0
  202. package/src/routing/types.ts +13 -1
  203. package/src/runtime/answer-contracts.ts +633 -0
  204. package/src/runtime/artifact-contracts.ts +76 -0
  205. package/src/runtime/planning-evidence.ts +591 -0
  206. package/src/runtime/session-coordinator.ts +78 -12
  207. package/src/system-prompt.ts +4 -1
  208. package/src/tools/fundamentals/company-overview.ts +1 -1
  209. package/src/tools/fundamentals/comps.ts +1 -1
  210. package/src/tools/fundamentals/dcf.ts +1 -1
  211. package/src/tools/fundamentals/earnings.ts +1 -1
  212. package/src/tools/fundamentals/financials.ts +1 -1
  213. package/src/tools/fundamentals/sec-filings.ts +25 -2
  214. package/src/tools/index.ts +3 -0
  215. package/src/tools/macro/fear-greed.ts +1 -1
  216. package/src/tools/macro/fred-data.ts +31 -5
  217. package/src/tools/market/crypto-history.ts +18 -2
  218. package/src/tools/market/crypto-price.ts +1 -1
  219. package/src/tools/market/search-ticker.ts +1 -1
  220. package/src/tools/market/stock-history.ts +1 -1
  221. package/src/tools/market/stock-quote.ts +1 -1
  222. package/src/tools/options/greeks.ts +0 -1
  223. package/src/tools/options/option-chain.ts +9 -4
  224. package/src/tools/portfolio/correlation.ts +1 -1
  225. package/src/tools/portfolio/holdings-overlap.ts +123 -0
  226. package/src/tools/portfolio/predictions.ts +1 -1
  227. package/src/tools/portfolio/risk-analysis.ts +1 -1
  228. package/src/tools/portfolio/tracker.ts +1 -1
  229. package/src/tools/portfolio/watchlist.ts +10 -4
  230. package/src/tools/sentiment/reddit-sentiment.ts +1 -1
  231. package/src/tools/sentiment/sentiment-summary.ts +62 -2
  232. package/src/tools/sentiment/twitter-sentiment.ts +1 -1
  233. package/src/tools/sentiment/web-search.ts +36 -3
  234. package/src/tools/sentiment/web-sentiment.ts +1 -1
  235. package/src/tools/technical/backtest.ts +50 -29
  236. package/src/tools/technical/indicators.ts +1 -3
  237. package/src/types/options.ts +17 -0
  238. package/src/types/portfolio.ts +32 -0
  239. package/src/workflows/compare-assets.ts +38 -2
  240. package/src/workflows/options-screener.ts +85 -7
  241. package/src/workflows/portfolio-builder.ts +7 -3
  242. package/dist/runtime/index.d.ts +0 -16
  243. package/dist/runtime/index.js +0 -10
  244. package/dist/runtime/index.js.map +0 -1
  245. package/dist/runtime/provider-ids.d.ts +0 -14
  246. package/dist/runtime/provider-ids.js +0 -14
  247. package/dist/runtime/provider-ids.js.map +0 -1
  248. package/gui/web/dist/assets/index-DBrWq43L.css +0 -1
  249. package/gui/web/dist/assets/index-RflHaj0y.js +0 -67
  250. package/src/runtime/index.ts +0 -55
  251. package/src/runtime/provider-ids.ts +0 -15
@@ -6,18 +6,25 @@ import {
6
6
  import { buildComprehensiveAnalysisDefinition } from "../analysts/orchestrator.js";
7
7
  import { getConfig } from "../config.js";
8
8
  import {
9
- classifyIntent,
9
+ classifyWithLegacyRules,
10
10
  createPiAiRouterClient,
11
11
  resolveOptionsScreenerSlots,
12
12
  resolvePortfolioSlots,
13
13
  route as routeLlm,
14
+ buildResolvedTurnContext,
14
15
  } from "../routing/index.js";
15
16
  import type {
16
17
  RouterInputContext,
17
18
  RouterLlmClient,
18
19
  RouterOutput,
19
20
  } from "../routing/router-types.js";
20
- import type { CompareAssetsSlots, SlotResolution } from "../routing/types.js";
21
+ import type { ResolvedTurnContext } from "../routing/turn-context.js";
22
+ import type {
23
+ CompareAssetsSlots,
24
+ ExtractedEntities,
25
+ SlotResolution,
26
+ SlotSource,
27
+ } from "../routing/types.js";
21
28
  import { buildAssumptionsBlockFromRouter } from "../prompts/workflow-prompts.js";
22
29
  import {
23
30
  buildPortfolioWorkflowDefinition,
@@ -76,6 +83,8 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
76
83
  const sessionPromptedSet = new Set<ProviderId>();
77
84
  let hardPromptFiredInWorkflow = false;
78
85
  const degradationAccumulator = createDegradationAccumulator();
86
+ let activeToolSnapshot: string[] | null = null;
87
+ let currentRouteToolContext: ResolvedTurnContext | null = null;
79
88
 
80
89
  // Register tools
81
90
  for (const tool of getOpenCandleToolDefinitions()) {
@@ -261,6 +270,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
261
270
  msg.role === "assistant" && msg.stopReason === "stop";
262
271
  if (isFinalAssistantTurn) {
263
272
  pi.appendEntry("opencandle-disclaimer", { text: DISCLAIMER_TEXT });
273
+ restoreRouteToolScope();
264
274
  }
265
275
 
266
276
  if (degradationAccumulator.isEmpty()) return;
@@ -454,6 +464,29 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
454
464
  return undefined;
455
465
  });
456
466
 
467
+ pi.on("tool_call", async (event) => {
468
+ if (!currentRouteToolContext) return undefined;
469
+ const allowed = new Set(currentRouteToolContext.activeToolNames);
470
+ if (allowed.has(event.toolName)) return undefined;
471
+
472
+ const diagnostic = {
473
+ routeKind: currentRouteToolContext.routeKind,
474
+ workflow: currentRouteToolContext.workflow,
475
+ toolName: event.toolName,
476
+ toolBundles: currentRouteToolContext.toolBundles,
477
+ activeToolNames: currentRouteToolContext.activeToolNames,
478
+ };
479
+ pi.appendEntry("opencandle-tool-scope-violation", diagnostic);
480
+
481
+ if (getConfig().toolScopeMode === "enforce") {
482
+ return {
483
+ block: true,
484
+ reason: `Tool ${event.toolName} is outside the route-selected OpenCandle tool bundle.`,
485
+ };
486
+ }
487
+ return undefined;
488
+ });
489
+
457
490
  // Input handling — branches on OPENCANDLE_ROUTER_MODE.
458
491
  pi.on("input", async (event, ctx) => {
459
492
  if (event.source === "extension") return;
@@ -462,8 +495,8 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
462
495
  const analysis = isAnalysisRequest(event.text);
463
496
  if (analysis.match && analysis.symbol) {
464
497
  const definition = buildComprehensiveAnalysisDefinition(analysis.symbol, { debate: getConfig().debate });
465
- coordinator.executeWorkflow(pi, definition, ctx);
466
- return { action: "handled" };
498
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
499
+ return prompt ? { action: "transform", text: prompt } : { action: "handled" };
467
500
  }
468
501
 
469
502
  const mode = getConfig().routerMode;
@@ -473,25 +506,25 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
473
506
  // the workflow's queued prompts; tell Pi not to also forward it.
474
507
  // Fallback path (no dispatch) → let Pi pass the user turn through to the
475
508
  // main agent, which will run under the router-supplied fallback context.
476
- return dispatched ? { action: "handled" } : undefined;
509
+ return dispatched || undefined;
477
510
  }
478
511
 
479
- // --- rules mode (default) ---
512
+ // --- explicit legacy rules mode (`OPENCANDLE_ROUTER_MODE=rules`) ---
480
513
  // Extract and persist user preferences (legacy regex path)
481
514
  coordinator.extractAndStorePreferences(event.text);
482
515
  const storage = coordinator.getStorage();
483
516
  const workflowPrefs = storage?.getWorkflowPreferences("global") ?? {};
484
517
 
485
518
  // Classify intent
486
- const classification = classifyIntent(event.text);
519
+ const classification = classifyWithLegacyRules(event.text);
487
520
 
488
521
  if (classification.workflow === "portfolio_builder") {
489
522
  const resolution = resolvePortfolioSlots(classification.entities, workflowPrefs);
490
523
  coordinator.recordWorkflowRun("portfolio_builder", classification.entities, resolution.resolved, resolution.defaultsUsed);
491
524
  pi.appendEntry("opencandle-workflow", { workflow: "portfolio_builder", entities: classification.entities, resolved: resolution.resolved });
492
525
  const definition = buildPortfolioWorkflowDefinition(resolution);
493
- coordinator.executeWorkflow(pi, definition, ctx);
494
- return { action: "handled" };
526
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
527
+ return prompt ? { action: "transform", text: prompt } : { action: "handled" };
495
528
  }
496
529
 
497
530
  if (classification.workflow === "options_screener") {
@@ -500,23 +533,31 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
500
533
  coordinator.recordWorkflowRun("options_screener", classification.entities, resolution.resolved, resolution.defaultsUsed);
501
534
  pi.appendEntry("opencandle-workflow", { workflow: "options_screener", entities: classification.entities, resolved: resolution.resolved });
502
535
  const definition = buildOptionsScreenerWorkflowDefinition(resolution);
503
- coordinator.executeWorkflow(pi, definition, ctx);
504
- return { action: "handled" };
536
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
537
+ return prompt ? { action: "transform", text: prompt } : { action: "handled" };
505
538
  }
506
539
  }
507
540
 
508
541
  if (classification.workflow === "compare_assets" && classification.entities.symbols.length >= 2) {
509
542
  const resolution: SlotResolution<CompareAssetsSlots> = {
510
- resolved: { symbols: classification.entities.symbols },
511
- sources: { symbols: "user" },
543
+ resolved: {
544
+ symbols: classification.entities.symbols,
545
+ metrics: classification.entities.compareMetrics,
546
+ timeHorizon: classification.entities.timeHorizon,
547
+ },
548
+ sources: {
549
+ symbols: "user",
550
+ ...(classification.entities.timeHorizon ? { timeHorizon: "user" as const } : {}),
551
+ ...(classification.entities.compareMetrics ? { metrics: "user" as const } : {}),
552
+ },
512
553
  defaultsUsed: [],
513
554
  missingRequired: [],
514
555
  };
515
556
  coordinator.recordWorkflowRun("compare_assets", classification.entities, resolution.resolved, resolution.defaultsUsed);
516
557
  pi.appendEntry("opencandle-workflow", { workflow: "compare_assets", symbols: classification.entities.symbols });
517
558
  const definition = buildCompareAssetsWorkflowDefinition(resolution);
518
- coordinator.executeWorkflow(pi, definition, ctx);
519
- return { action: "handled" };
559
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
560
+ return prompt ? { action: "transform", text: prompt } : { action: "handled" };
520
561
  }
521
562
  });
522
563
 
@@ -530,7 +571,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
530
571
  async function handleLlmRouterTurn(
531
572
  text: string,
532
573
  ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
533
- ): Promise<boolean> {
574
+ ): Promise<{ action: "transform"; text: string } | false> {
534
575
  const storage = coordinator.getStorage();
535
576
  const { profileSnapshot, recentWorkflowRuns, priorTurns } =
536
577
  coordinator.buildRouterContextBase(ctx.sessionManager);
@@ -563,7 +604,33 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
563
604
  return false;
564
605
  }
565
606
 
607
+ const availableToolNames = safeGetAllToolNames();
608
+ const memory = coordinator.retrieveMemoryForRoute(
609
+ output.routeKind,
610
+ output.workflow,
611
+ Object.keys(output.slots),
612
+ );
613
+ const resolvedTurnContext = buildResolvedTurnContext(input, output, {
614
+ availableToolNames,
615
+ memoryEntries: memory.entries,
616
+ filteredMemory: memory.filtered.map(({ entry, reason }) => ({
617
+ category: entry.category,
618
+ key: entry.key,
619
+ source: entry.source,
620
+ recordedAt: entry.recordedAt,
621
+ confidence: entry.confidence,
622
+ filtered: true,
623
+ filterReason: reason,
624
+ })),
625
+ planning: {
626
+ migrationStatuses: getConfig().planningMigrationStatuses,
627
+ },
628
+ });
629
+
566
630
  pi.appendEntry("opencandle-router", { output });
631
+ pi.appendEntry("opencandle-route-context", resolvedTurnContext);
632
+ coordinator.setPendingResolvedTurnContext(resolvedTurnContext);
633
+ applyRouteToolScope(resolvedTurnContext);
567
634
 
568
635
  // Preference writes: HIGH-confidence only. Medium/low are logged for
569
636
  // observability even when no storage is available.
@@ -585,10 +652,14 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
585
652
  }
586
653
 
587
654
  // Workflow dispatch for recognised workflows.
588
- if (output.route === "workflow" && output.workflow) {
655
+ if (output.routeKind === "workflow_dispatch" && output.workflow) {
589
656
  return dispatchRouterWorkflow(output, ctx);
590
657
  }
591
658
 
659
+ if (output.routeKind === "pass_through") {
660
+ return false;
661
+ }
662
+
592
663
  // Fallback: record the turn and stash the fallback context for the
593
664
  // upcoming `before_agent_start` hook to render into the system prompt.
594
665
  coordinator.recordWorkflowRun(
@@ -596,7 +667,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
596
667
  output.entities,
597
668
  Object.fromEntries(Object.entries(output.slots).map(([k, v]) => [k, v.value])),
598
669
  [],
599
- "fallback",
670
+ output.routeKind,
600
671
  );
601
672
 
602
673
  const assumptionsBlock = buildAssumptionsBlockFromRouter(output.slots);
@@ -605,6 +676,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
605
676
  missingRequired: output.missing_required,
606
677
  extraContext: output.entities.symbols.length > 0
607
678
  ? `Router-extracted symbols: ${output.entities.symbols.join(", ")}.`
679
+ + ` Route kind: ${output.routeKind}. Tool bundles: ${output.tool_bundles.join(", ") || "(none)"}.`
608
680
  : undefined,
609
681
  });
610
682
  return false;
@@ -613,73 +685,88 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
613
685
  function dispatchRouterWorkflow(
614
686
  output: RouterOutput,
615
687
  ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
616
- ): boolean {
688
+ ): { action: "transform"; text: string } | false {
617
689
  const workflow = output.workflow!;
618
690
  const storage = coordinator.getStorage();
619
691
  const workflowPrefs = storage?.getWorkflowPreferences("global") ?? {};
692
+ const entities = mergeRouterSlotsIntoEntities(output);
620
693
 
621
694
  if (workflow === "portfolio_builder") {
622
- const resolution = resolvePortfolioSlots(output.entities, workflowPrefs);
695
+ const resolution = withRouterSlotSources(
696
+ resolvePortfolioSlots(entities, workflowPrefs),
697
+ output,
698
+ );
623
699
  coordinator.recordWorkflowRun(
624
700
  "portfolio_builder",
625
- output.entities,
701
+ entities,
626
702
  resolution.resolved,
627
703
  resolution.defaultsUsed,
628
- "workflow",
704
+ output.routeKind,
629
705
  );
630
706
  pi.appendEntry("opencandle-workflow", {
631
707
  workflow: "portfolio_builder",
632
- entities: output.entities,
708
+ entities,
633
709
  resolved: resolution.resolved,
634
710
  });
635
711
  const definition = buildPortfolioWorkflowDefinition(resolution);
636
- coordinator.executeWorkflow(pi, definition, ctx);
637
- return true;
712
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
713
+ return prompt ? { action: "transform", text: prompt } : false;
638
714
  }
639
715
  if (workflow === "options_screener") {
640
- const resolution = resolveOptionsScreenerSlots(output.entities, workflowPrefs);
716
+ const resolution = withRouterSlotSources(
717
+ resolveOptionsScreenerSlots(entities, workflowPrefs),
718
+ output,
719
+ );
641
720
  // Router may emit missing_required; main agent handles via ask_user.
642
721
  // Still dispatch the workflow when symbol is present.
643
722
  if (resolution.missingRequired.length === 0) {
644
723
  coordinator.recordWorkflowRun(
645
724
  "options_screener",
646
- output.entities,
725
+ entities,
647
726
  resolution.resolved,
648
727
  resolution.defaultsUsed,
649
- "workflow",
728
+ output.routeKind,
650
729
  );
651
730
  pi.appendEntry("opencandle-workflow", {
652
731
  workflow: "options_screener",
653
- entities: output.entities,
732
+ entities,
654
733
  resolved: resolution.resolved,
655
734
  });
656
735
  const definition = buildOptionsScreenerWorkflowDefinition(resolution);
657
- coordinator.executeWorkflow(pi, definition, ctx);
658
- return true;
736
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
737
+ return prompt ? { action: "transform", text: prompt } : false;
659
738
  }
660
739
  // Missing required symbol — treat as fallback with ask_user directive.
661
740
  }
662
- if (workflow === "compare_assets" && output.entities.symbols.length >= 2) {
741
+ if (workflow === "compare_assets" && entities.symbols.length >= 2) {
663
742
  const resolution: SlotResolution<CompareAssetsSlots> = {
664
- resolved: { symbols: output.entities.symbols },
665
- sources: { symbols: "user" },
743
+ resolved: {
744
+ symbols: entities.symbols,
745
+ metrics: entities.compareMetrics,
746
+ timeHorizon: entities.timeHorizon,
747
+ },
748
+ sources: {
749
+ symbols: sourceForRouterSlot(output, "symbols", "user"),
750
+ ...(entities.timeHorizon ? { timeHorizon: "user" as const } : {}),
751
+ ...(entities.compareMetrics ? { metrics: "user" as const } : {}),
752
+ },
666
753
  defaultsUsed: [],
667
754
  missingRequired: [],
668
755
  };
669
756
  coordinator.recordWorkflowRun(
670
757
  "compare_assets",
671
- output.entities,
758
+ entities,
672
759
  resolution.resolved,
673
760
  [],
674
- "workflow",
761
+ output.routeKind,
675
762
  );
676
763
  pi.appendEntry("opencandle-workflow", {
677
764
  workflow: "compare_assets",
678
- symbols: output.entities.symbols,
765
+ symbols: entities.symbols,
679
766
  });
680
767
  const definition = buildCompareAssetsWorkflowDefinition(resolution);
681
- coordinator.executeWorkflow(pi, definition, ctx);
682
- return true;
768
+ const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
769
+ return prompt ? { action: "transform", text: prompt } : false;
683
770
  }
684
771
 
685
772
  // single_asset_analysis / watchlist / general_qa + any workflow with
@@ -690,17 +777,155 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
690
777
  output.entities,
691
778
  Object.fromEntries(Object.entries(output.slots).map(([k, v]) => [k, v.value])),
692
779
  [],
693
- "fallback",
780
+ output.routeKind,
694
781
  );
695
782
  const assumptionsBlock = buildAssumptionsBlockFromRouter(output.slots);
696
783
  coordinator.setPendingFallbackContext({
697
784
  assumptionsBlock,
698
785
  missingRequired: output.missing_required,
699
- extraContext: `Router classified as ${workflow} but declined to dispatch. Symbols: ${output.entities.symbols.join(", ") || "(none)"}.`,
786
+ extraContext: `Router classified as ${workflow} but declined to dispatch. Symbols: ${entities.symbols.join(", ") || "(none)"}.`,
700
787
  });
701
788
  return false;
702
789
  }
703
790
 
791
+ function mergeRouterSlotsIntoEntities(output: RouterOutput): ExtractedEntities {
792
+ const entities: ExtractedEntities = {
793
+ ...output.entities,
794
+ symbols: output.entities.symbols,
795
+ };
796
+
797
+ if (entities.budget === undefined && typeof output.slots.budget?.value === "number") {
798
+ entities.budget = output.slots.budget.value;
799
+ }
800
+
801
+ const slotSymbols = symbolsFromRouterSlots(output);
802
+ if (slotSymbols.length > 0 && slotSymbols.length > entities.symbols.length) {
803
+ entities.symbols = mergeSymbols(slotSymbols, entities.symbols);
804
+ }
805
+
806
+ return entities;
807
+ }
808
+
809
+ function withRouterSlotSources<T extends object>(
810
+ resolution: SlotResolution<T>,
811
+ output: RouterOutput,
812
+ ): SlotResolution<T> {
813
+ const sources: Record<string, SlotSource | undefined> = { ...resolution.sources };
814
+ if (output.entities.budget === undefined && output.slots.budget) {
815
+ sources.budget = output.slots.budget.source;
816
+ }
817
+ if (output.entities.symbols.length === 0 && output.slots.symbol) {
818
+ sources.symbol = output.slots.symbol.source;
819
+ }
820
+ if (output.entities.symbols.length < 2 && output.slots.symbols) {
821
+ sources.symbols = output.slots.symbols.source;
822
+ }
823
+ return { ...resolution, sources: sources as SlotResolution<T>["sources"] };
824
+ }
825
+
826
+ function sourceForRouterSlot(
827
+ output: RouterOutput,
828
+ slotName: "symbol" | "symbols" | "budget",
829
+ fallback: SlotSource,
830
+ ): SlotSource {
831
+ return output.slots[slotName]?.source ?? fallback;
832
+ }
833
+
834
+ function symbolsFromRouterSlots(output: RouterOutput): string[] {
835
+ const symbols: string[] = [];
836
+ const symbol = output.slots.symbol?.value;
837
+ if (typeof symbol === "string" && symbol.trim() !== "") {
838
+ symbols.push(symbol.toUpperCase());
839
+ }
840
+ const symbolList = output.slots.symbols?.value;
841
+ if (Array.isArray(symbolList)) {
842
+ for (const value of symbolList) {
843
+ if (typeof value === "string" && value.trim() !== "") {
844
+ symbols.push(value.toUpperCase());
845
+ }
846
+ }
847
+ }
848
+ return symbols;
849
+ }
850
+
851
+ function mergeSymbols(primary: string[], secondary: string[]): string[] {
852
+ const merged: string[] = [];
853
+ for (const symbol of [...primary, ...secondary]) {
854
+ if (!merged.includes(symbol)) merged.push(symbol);
855
+ }
856
+ return merged;
857
+ }
858
+
859
+ function safeGetAllToolNames(): string[] {
860
+ try {
861
+ return pi.getAllTools().map((tool) => tool.name);
862
+ } catch {
863
+ return [];
864
+ }
865
+ }
866
+
867
+ function applyRouteToolScope(context: ResolvedTurnContext): void {
868
+ const mode = getConfig().toolScopeMode;
869
+ currentRouteToolContext = context;
870
+ pi.appendEntry("opencandle-tool-scope", {
871
+ mode,
872
+ routeKind: context.routeKind,
873
+ workflow: context.workflow,
874
+ toolBundles: context.toolBundles,
875
+ activeToolNames: context.activeToolNames,
876
+ enforced: false,
877
+ });
878
+
879
+ if (mode !== "enforce") return;
880
+ if (context.activeToolNames.length === 0) return;
881
+
882
+ try {
883
+ if (activeToolSnapshot === null) {
884
+ activeToolSnapshot = pi.getActiveTools();
885
+ }
886
+ pi.setActiveTools(context.activeToolNames);
887
+ pi.appendEntry("opencandle-tool-scope", {
888
+ mode,
889
+ routeKind: context.routeKind,
890
+ workflow: context.workflow,
891
+ toolBundles: context.toolBundles,
892
+ activeToolNames: context.activeToolNames,
893
+ enforced: true,
894
+ });
895
+ } catch (err) {
896
+ pi.appendEntry("opencandle-tool-scope", {
897
+ mode,
898
+ routeKind: context.routeKind,
899
+ workflow: context.workflow,
900
+ toolBundles: context.toolBundles,
901
+ activeToolNames: context.activeToolNames,
902
+ enforced: false,
903
+ diagnostic: err instanceof Error ? err.message : String(err),
904
+ });
905
+ }
906
+ }
907
+
908
+ function restoreRouteToolScope(): void {
909
+ currentRouteToolContext = null;
910
+ if (activeToolSnapshot === null) return;
911
+ try {
912
+ pi.setActiveTools(activeToolSnapshot);
913
+ pi.appendEntry("opencandle-tool-scope", {
914
+ mode: getConfig().toolScopeMode,
915
+ restored: true,
916
+ activeToolNames: activeToolSnapshot,
917
+ });
918
+ } catch (err) {
919
+ pi.appendEntry("opencandle-tool-scope", {
920
+ mode: getConfig().toolScopeMode,
921
+ restored: false,
922
+ diagnostic: err instanceof Error ? err.message : String(err),
923
+ });
924
+ } finally {
925
+ activeToolSnapshot = null;
926
+ }
927
+ }
928
+
704
929
  function resolveRouterLlmClient(
705
930
  ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
706
931
  ): RouterLlmClient | null {
@@ -717,8 +942,14 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
717
942
  // is pending (router-mode fallback turns), inject it into the prompt.
718
943
  pi.on("before_agent_start", async (event) => {
719
944
  const fallbackContext = coordinator.consumePendingFallbackContext() ?? undefined;
945
+ const resolvedTurnContext = coordinator.consumePendingResolvedTurnContext() ?? undefined;
720
946
  return {
721
- systemPrompt: coordinator.buildSystemPrompt(event.systemPrompt, undefined, fallbackContext),
947
+ systemPrompt: coordinator.buildSystemPrompt(
948
+ event.systemPrompt,
949
+ undefined,
950
+ fallbackContext,
951
+ resolvedTurnContext,
952
+ ),
722
953
  };
723
954
  });
724
955
  }
package/src/pi/setup.ts CHANGED
@@ -158,8 +158,18 @@ async function runLoginDialog(ctx: ExtensionContext, providerId: string): Promis
158
158
  dialog.showWaiting("Waiting for browser authentication...");
159
159
  }
160
160
  },
161
+ onDeviceCode: (info) => {
162
+ dialog.showDeviceCode(info);
163
+ dialog.showWaiting("Waiting for authentication...");
164
+ },
161
165
  onPrompt: async (prompt) => dialog.showPrompt(prompt.message, prompt.placeholder),
162
166
  onProgress: (message) => dialog.showProgress(message),
167
+ onSelect: async (prompt) => {
168
+ const options = prompt.options.map((option, index) => `${index + 1}. ${option.label}`).join("\n");
169
+ const answer = await dialog.showPrompt(`${prompt.message}\n\n${options}`, "Enter a number");
170
+ const selectedIndex = Number.parseInt(answer.trim(), 10) - 1;
171
+ return prompt.options[selectedIndex]?.id;
172
+ },
163
173
  onManualCodeInput: () => manualCodePromise,
164
174
  signal: dialog.signal,
165
175
  })