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
@@ -8,8 +8,8 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
- <script type="module" crossorigin src="/assets/index-RflHaj0y.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-DBrWq43L.css">
11
+ <script type="module" crossorigin src="/assets/index-CZ9DHZYy.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-Bxt9QpLX.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencandle",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Financial trading & investing agent",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/Kahtaf/OpenCandle#readme",
@@ -13,7 +13,8 @@
13
13
  },
14
14
  "type": "module",
15
15
  "workspaces": [
16
- "gui/*"
16
+ "gui/server",
17
+ "gui/web"
17
18
  ],
18
19
  "bin": {
19
20
  "opencandle": "dist/cli.js"
@@ -85,6 +86,8 @@
85
86
  "prestart": "npm run check:node",
86
87
  "start": "tsx src/cli.ts",
87
88
  "gui:web:build": "npm --workspace @opencandle/gui-web run build",
89
+ "docs:site:build": "node website/build.mjs",
90
+ "docs:site:serve": "node website/serve.mjs",
88
91
  "gui": "tsx gui/server/server.ts",
89
92
  "gui:dev": "tsx gui/server/server.ts",
90
93
  "pretest": "npm run check:node",
@@ -101,6 +104,9 @@
101
104
  "test:evals": "vitest run --config vitest.config.evals.ts",
102
105
  "eval:router-live": "tsx tests/scripts/run-live-router-eval.ts",
103
106
  "test:evals:usually": "EVAL_TIER=usually vitest run --config vitest.config.evals.ts",
107
+ "test:evals:product": "tsx tests/scripts/run-product-evals.ts",
108
+ "test:evals:competitive": "tsx tests/scripts/run-competitive-finance-eval.ts",
109
+ "eval:competitive:analyze": "tsx tests/scripts/analyze-competitive-finance-report.ts",
104
110
  "version:patch": "npm version patch --no-git-tag-version",
105
111
  "version:minor": "npm version minor --no-git-tag-version",
106
112
  "version:major": "npm version major --no-git-tag-version",
@@ -115,29 +121,29 @@
115
121
  "node": "^20.19.0 || ^22.12.0 || >=24.0.0 <27"
116
122
  },
117
123
  "dependencies": {
118
- "@earendil-works/pi-agent-core": "^0.74.0",
119
- "@earendil-works/pi-ai": "^0.74.0",
120
- "@earendil-works/pi-coding-agent": "^0.74.0",
124
+ "@earendil-works/pi-agent-core": "^0.75.5",
125
+ "@earendil-works/pi-ai": "^0.75.5",
126
+ "@earendil-works/pi-coding-agent": "^0.75.5",
121
127
  "@the-convocation/twitter-scraper": "^0.22.3",
122
128
  "better-sqlite3": "^12.10.0",
123
129
  "camoufox-js": "^0.10.2",
124
130
  "duck-duck-scrape": "^2.2.7",
125
131
  "playwright-core": "^1.60.0",
126
- "tsx": "^4.22.0"
132
+ "tsx": "^4.22.3"
127
133
  },
128
134
  "peerDependencies": {
129
135
  "@sinclair/typebox": "*"
130
136
  },
131
137
  "devDependencies": {
132
- "@mariozechner/pi-agent-core": "^0.73.1",
133
- "@mariozechner/pi-ai": "^0.73.1",
134
- "@mariozechner/pi-coding-agent": "^0.73.1",
138
+ "@agentclientprotocol/claude-agent-acp": "^0.37.0",
135
139
  "@sinclair/typebox": "^0.34.0",
136
140
  "@types/better-sqlite3": "^7.6.13",
137
141
  "@types/node": "^22.19.19",
142
+ "@zed-industries/codex-acp": "^0.15.0",
143
+ "acpx": "^0.10.0",
138
144
  "typescript": "^6.0.3",
139
- "vite": "^8.0.11",
140
- "vitest": "^4.1.6",
141
- "vitest-evals": "^0.8.0"
145
+ "vite": "^8.0.14",
146
+ "vitest": "^4.1.7",
147
+ "vitest-evals": "^0.11.0"
142
148
  }
143
149
  }
package/src/cli.ts CHANGED
@@ -140,6 +140,7 @@ async function handleGuiCommand(args: string[], cwd: string): Promise<boolean> {
140
140
  }
141
141
 
142
142
  async function main(): Promise<void> {
143
+ const rawArgs = process.argv.slice(2);
143
144
  const { positionals } = parseArgs({ allowPositionals: true, strict: false });
144
145
  const cwd = process.cwd();
145
146
  const agentDir = getAgentDir();
@@ -148,7 +149,7 @@ async function main(): Promise<void> {
148
149
  return;
149
150
  }
150
151
 
151
- if (await handlePackageCommand(positionals, cwd, agentDir)) {
152
+ if (await handlePackageCommand(rawArgs, cwd, agentDir)) {
152
153
  return;
153
154
  }
154
155
 
package/src/config.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { ensureParentDir, getConfigPath } from "./infra/opencandle-paths.js";
3
+ import type { PlanningBehaviorMode, TaskFamily } from "./routing/planning.js";
3
4
 
4
5
  export interface SentimentConfig {
5
6
  retentionDays: number;
@@ -9,6 +10,8 @@ export interface SentimentConfig {
9
10
  }
10
11
 
11
12
  export type RouterMode = "rules" | "llm";
13
+ export type ToolScopeMode = "observe" | "enforce";
14
+ export type PlanningMigrationStatuses = Partial<Record<TaskFamily, PlanningBehaviorMode>>;
12
15
 
13
16
  export interface Config {
14
17
  alphaVantageApiKey?: string;
@@ -19,11 +22,24 @@ export interface Config {
19
22
  /** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
20
23
  debate?: boolean;
21
24
  /**
22
- * Intent-router rollout flag. `"rules"` (default) runs the legacy regex
23
- * `classifyIntent` + `extractPreferences` path. `"llm"` runs the LLM router
24
- * ahead of prompt assembly. Controlled by `OPENCANDLE_ROUTER_MODE`.
25
+ * Intent-router mode. `"llm"` (default) runs the LLM router ahead of prompt
26
+ * assembly. `"rules"` is the explicit legacy rule-router rollback path
27
+ * (`classifyIntent` + `extractPreferences`). Controlled by
28
+ * `OPENCANDLE_ROUTER_MODE`.
25
29
  */
26
30
  routerMode: RouterMode;
31
+ /**
32
+ * Route-selected tool scope mode. `"observe"` (default) records selected
33
+ * bundles and active-tool candidates. `"enforce"` applies Pi active tools
34
+ * for the turn via `pi.setActiveTools`.
35
+ */
36
+ toolScopeMode: ToolScopeMode;
37
+ /**
38
+ * Per-task planning behavior rollback/activation overrides. Controlled by
39
+ * `OPENCANDLE_PLANNING_MIGRATION_STATUSES`, e.g.
40
+ * `asset_compare=dual_run,single_asset_decision=observe_only`.
41
+ */
42
+ planningMigrationStatuses?: PlanningMigrationStatuses;
27
43
  sentiment?: SentimentConfig;
28
44
  }
29
45
 
@@ -84,15 +100,81 @@ const SENTIMENT_DEFAULTS: SentimentConfig = {
84
100
  divergenceThreshold: 0.4,
85
101
  };
86
102
 
103
+ const PLANNING_TASK_FAMILIES = [
104
+ "single_asset_decision",
105
+ "asset_compare",
106
+ "portfolio_build",
107
+ "portfolio_review",
108
+ "macro_allocation_review",
109
+ "options_strategy",
110
+ "current_event_explanation",
111
+ "ticker_disambiguation",
112
+ "filing_thesis_review",
113
+ "sentiment_snapshot",
114
+ "concept_explainer",
115
+ "retail_finance_tradeoff",
116
+ "stateful_tracking_update",
117
+ "backtest_review",
118
+ "general_fallback",
119
+ ] as const satisfies readonly TaskFamily[];
120
+
121
+ const PLANNING_BEHAVIOR_MODES = [
122
+ "observe_only",
123
+ "dual_run",
124
+ "replacement_active",
125
+ ] as const satisfies readonly PlanningBehaviorMode[];
126
+
87
127
  function resolveRouterMode(): RouterMode {
88
128
  const raw = process.env.OPENCANDLE_ROUTER_MODE;
89
- if (raw === undefined || raw === "") return "rules";
129
+ if (raw === undefined || raw === "") return "llm";
90
130
  if (raw === "rules" || raw === "llm") return raw;
91
131
  throw new Error(
92
- `Invalid OPENCANDLE_ROUTER_MODE="${raw}". Allowed values: "rules" (default) or "llm".`,
132
+ `Invalid OPENCANDLE_ROUTER_MODE="${raw}". Allowed values: "llm" (default) or "rules".`,
133
+ );
134
+ }
135
+
136
+ function resolveToolScopeMode(): ToolScopeMode {
137
+ const raw = process.env.OPENCANDLE_TOOL_SCOPE_MODE;
138
+ if (raw === undefined || raw === "") return "observe";
139
+ if (raw === "observe" || raw === "enforce") return raw;
140
+ throw new Error(
141
+ `Invalid OPENCANDLE_TOOL_SCOPE_MODE="${raw}". Allowed values: "observe" (default) or "enforce".`,
93
142
  );
94
143
  }
95
144
 
145
+ function resolvePlanningMigrationStatuses(): PlanningMigrationStatuses | undefined {
146
+ const raw = process.env.OPENCANDLE_PLANNING_MIGRATION_STATUSES;
147
+ if (raw === undefined || raw.trim() === "") return undefined;
148
+
149
+ const statuses: PlanningMigrationStatuses = {};
150
+ for (const entry of raw.split(",")) {
151
+ const trimmed = entry.trim();
152
+ if (!trimmed) continue;
153
+
154
+ const parts = trimmed.split("=");
155
+ const taskFamily = parts[0]?.trim();
156
+ const behaviorMode = parts[1]?.trim();
157
+ if (
158
+ parts.length !== 2 ||
159
+ !isPlanningTaskFamily(taskFamily) ||
160
+ !isPlanningBehaviorMode(behaviorMode)
161
+ ) {
162
+ throw new Error(`Invalid OPENCANDLE_PLANNING_MIGRATION_STATUSES entry "${trimmed}".`);
163
+ }
164
+ statuses[taskFamily] = behaviorMode;
165
+ }
166
+
167
+ return Object.keys(statuses).length > 0 ? statuses : undefined;
168
+ }
169
+
170
+ function isPlanningTaskFamily(value: string | undefined): value is TaskFamily {
171
+ return PLANNING_TASK_FAMILIES.includes(value as TaskFamily);
172
+ }
173
+
174
+ function isPlanningBehaviorMode(value: string | undefined): value is PlanningBehaviorMode {
175
+ return PLANNING_BEHAVIOR_MODES.includes(value as PlanningBehaviorMode);
176
+ }
177
+
96
178
  function resolveConfig(fileConfig: OpenCandleFileConfig): Config {
97
179
  const debateEnv = process.env.OPENCANDLE_DEBATE;
98
180
  const fileSentiment = fileConfig.sentiment;
@@ -105,6 +187,8 @@ function resolveConfig(fileConfig: OpenCandleFileConfig): Config {
105
187
  finnhubApiKey: process.env.FINNHUB_API_KEY ?? fileConfig.providers?.finnhub?.apiKey,
106
188
  debate: debateEnv !== undefined ? debateEnv !== "false" && debateEnv !== "0" : fileConfig.debate ?? true,
107
189
  routerMode: resolveRouterMode(),
190
+ toolScopeMode: resolveToolScopeMode(),
191
+ planningMigrationStatuses: resolvePlanningMigrationStatuses(),
108
192
  sentiment: {
109
193
  retentionDays: fileSentiment?.retentionDays ?? SENTIMENT_DEFAULTS.retentionDays,
110
194
  defaultSubreddits: fileSentiment?.defaultSubreddits ?? SENTIMENT_DEFAULTS.defaultSubreddits,
@@ -65,7 +65,7 @@ export const StealthBrowser = {
65
65
  * Fetch JSON from a URL using the browser's session (cookies, TLS fingerprint).
66
66
  * Useful for APIs that block Node.js fetch but allow real browsers.
67
67
  */
68
- async fetchJson<T>(url: string, options?: { cookies?: string }): Promise<T> {
68
+ async fetchJson<T>(url: string): Promise<T> {
69
69
  return withPage(async (p) => {
70
70
  const result = await p.evaluate(async (fetchUrl: string) => {
71
71
  const res = await fetch(fetchUrl, { credentials: "include" });
@@ -9,6 +9,11 @@ interface Bucket {
9
9
  config: BucketConfig;
10
10
  }
11
11
 
12
+ export const ALPHA_VANTAGE_RATE_LIMIT = {
13
+ maxTokens: 5,
14
+ refillRate: 0.083,
15
+ } as const;
16
+
12
17
  export class RateLimiter {
13
18
  private buckets = new Map<string, Bucket>();
14
19
 
@@ -53,7 +58,11 @@ export class RateLimiter {
53
58
  export const rateLimiter = new RateLimiter();
54
59
  rateLimiter.configure("yahoo", 5, 5); // 5 req/s
55
60
  rateLimiter.configure("coingecko", 10, 0.167); // 10 req/min
56
- rateLimiter.configure("alphavantage", 5, 0.083); // 5 req/min (free tier)
61
+ rateLimiter.configure(
62
+ "alphavantage",
63
+ ALPHA_VANTAGE_RATE_LIMIT.maxTokens,
64
+ ALPHA_VANTAGE_RATE_LIMIT.refillRate,
65
+ ); // 5 req/min (free tier)
57
66
  rateLimiter.configure("fred", 120, 2); // 120 req/min
58
67
  rateLimiter.configure("twitter", 5, 0.167); // 5 req, ~10 req/min
59
68
  rateLimiter.configure("reddit", 5, 0.167); // 5 req, ~10 req/min
@@ -7,6 +7,16 @@ import {
7
7
  isStale,
8
8
  } from "./types.js";
9
9
 
10
+ export interface FilteredMemoryEntry {
11
+ entry: MemoryEntry;
12
+ reason: "suppressed_by_user_slot" | "never_trust" | "stale" | "irrelevant_category";
13
+ }
14
+
15
+ export interface MemoryRetrievalResult {
16
+ entries: MemoryEntry[];
17
+ filtered: FilteredMemoryEntry[];
18
+ }
19
+
10
20
  /** Slot name → preference key(s) mapping for suppression. */
11
21
  const SLOT_TO_PREF_KEYS: Record<string, string[]> = {
12
22
  riskProfile: ["risk_profile"],
@@ -36,6 +46,14 @@ export class MemoryManager {
36
46
  overriddenSlots?: string[],
37
47
  now: Date = new Date(),
38
48
  ): MemoryEntry[] {
49
+ return this.retrieveDetailed(workflowType, overriddenSlots, now).entries;
50
+ }
51
+
52
+ retrieveDetailed(
53
+ workflowType: string,
54
+ overriddenSlots?: string[],
55
+ now: Date = new Date(),
56
+ ): MemoryRetrievalResult {
39
57
  const relevantCategories = WORKFLOW_RELEVANT_CATEGORIES[workflowType] ??
40
58
  WORKFLOW_RELEVANT_CATEGORIES["unclassified"];
41
59
 
@@ -49,18 +67,14 @@ export class MemoryManager {
49
67
  }
50
68
 
51
69
  const entries: MemoryEntry[] = [];
70
+ const filtered: FilteredMemoryEntry[] = [];
52
71
 
53
72
  // Preferences as investor_profile entries
54
73
  if (relevantCategories.includes("investor_profile")) {
55
74
  const prefs = this.storage.getPreferencesByNamespace("global");
56
75
  for (const pref of prefs) {
57
76
  const key = String(pref.key);
58
- if (suppressedKeys.has(key)) continue;
59
- if (NEVER_TRUST_FROM_MEMORY.has(key)) continue;
60
-
61
77
  const category = KEY_TO_CATEGORY[key] ?? "investor_profile";
62
- if (!relevantCategories.includes(category)) continue;
63
-
64
78
  const entry: MemoryEntry = {
65
79
  key,
66
80
  value: tryParseValue(String(pref.value_json ?? "")),
@@ -70,9 +84,23 @@ export class MemoryManager {
70
84
  source: pref.source != null ? String(pref.source) : undefined,
71
85
  };
72
86
 
73
- if (!isStale(entry, now)) {
74
- entries.push(entry);
87
+ if (suppressedKeys.has(key)) {
88
+ filtered.push({ entry, reason: "suppressed_by_user_slot" });
89
+ continue;
90
+ }
91
+ if (NEVER_TRUST_FROM_MEMORY.has(key)) {
92
+ filtered.push({ entry, reason: "never_trust" });
93
+ continue;
94
+ }
95
+ if (!relevantCategories.includes(category)) {
96
+ filtered.push({ entry, reason: "irrelevant_category" });
97
+ continue;
98
+ }
99
+ if (isStale(entry, now)) {
100
+ filtered.push({ entry, reason: "stale" });
101
+ continue;
75
102
  }
103
+ entries.push(entry);
76
104
  }
77
105
  }
78
106
 
@@ -97,13 +125,18 @@ export class MemoryManager {
97
125
  recordedAt,
98
126
  };
99
127
 
100
- if (!isStale(entry, now)) {
101
- entries.push(entry);
128
+ if (isStale(entry, now)) {
129
+ filtered.push({ entry, reason: "stale" });
130
+ continue;
102
131
  }
132
+ entries.push(entry);
103
133
  }
104
134
  }
105
135
 
106
- return entries.slice(0, MAX_PREFERENCE_LINES + MAX_WORKFLOW_HISTORY_PER_TYPE * 4);
136
+ return {
137
+ entries: entries.slice(0, MAX_PREFERENCE_LINES + MAX_WORKFLOW_HISTORY_PER_TYPE * 4),
138
+ filtered,
139
+ };
107
140
  }
108
141
 
109
142
  /**
@@ -28,11 +28,12 @@ interface WorkflowRunInput {
28
28
  defaultsUsedJson: string;
29
29
  outputSummary?: string;
30
30
  /**
31
- * Router route verbatim. One of `"workflow"` or `"fallback"`. Defaults to
31
+ * Router route verbatim. Legacy rows contain `"workflow"` or `"fallback"`;
32
+ * typed-router rows may contain canonical route kinds. Defaults to
32
33
  * `"workflow"` at the schema layer so legacy callers (rules-mode cascade)
33
34
  * don't need to pass anything. Router-mode callers MUST pass this explicitly.
34
35
  */
35
- turnType?: "workflow" | "fallback";
36
+ turnType?: string;
36
37
  }
37
38
 
38
39
  interface RecommendationInput {
@@ -46,6 +46,10 @@ export const WORKFLOW_RELEVANT_CATEGORIES: Record<string, MemoryCategory[]> = {
46
46
  comprehensive_analysis: ["investor_profile", "workflow_history"],
47
47
  single_asset_analysis: ["investor_profile"],
48
48
  general_finance_qa: ["investor_profile"],
49
+ workflow_dispatch: ["investor_profile", "workflow_history"],
50
+ agent_task: ["investor_profile", "workflow_history"],
51
+ clarification: ["investor_profile", "workflow_history"],
52
+ pass_through: [],
49
53
  unclassified: ["investor_profile"],
50
54
  };
51
55