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.
- package/LICENSE +1 -1
- package/README.md +186 -117
- package/dist/analysts/contracts.d.ts +1 -3
- package/dist/analysts/contracts.js +1 -11
- package/dist/analysts/contracts.js.map +1 -1
- package/dist/analysts/orchestrator.d.ts +1 -3
- package/dist/analysts/orchestrator.js +1 -26
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli.js +32 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -3
- package/dist/config.js +69 -3
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infra/browser.d.ts +1 -3
- package/dist/infra/browser.js +4 -2
- package/dist/infra/browser.js.map +1 -1
- package/dist/infra/cache.d.ts +8 -11
- package/dist/infra/cache.js +17 -15
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/http-client.d.ts +4 -1
- package/dist/infra/http-client.js +59 -6
- package/dist/infra/http-client.js.map +1 -1
- package/dist/infra/index.d.ts +3 -3
- package/dist/infra/index.js +3 -3
- package/dist/infra/index.js.map +1 -1
- package/dist/infra/native-dependencies.js +2 -2
- package/dist/infra/native-dependencies.js.map +1 -1
- package/dist/infra/node-version.js.map +1 -1
- package/dist/infra/opencandle-paths.d.ts +0 -3
- package/dist/infra/opencandle-paths.js +4 -11
- package/dist/infra/opencandle-paths.js.map +1 -1
- package/dist/infra/rate-limiter.d.ts +4 -0
- package/dist/infra/rate-limiter.js +17 -10
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/alert-conditions.d.ts +34 -0
- package/dist/market-state/alert-conditions.js +23 -0
- package/dist/market-state/alert-conditions.js.map +1 -0
- package/dist/market-state/alert-runner.d.ts +55 -0
- package/dist/market-state/alert-runner.js +634 -0
- package/dist/market-state/alert-runner.js.map +1 -0
- package/dist/market-state/daily-report.d.ts +26 -0
- package/dist/market-state/daily-report.js +179 -0
- package/dist/market-state/daily-report.js.map +1 -0
- package/dist/market-state/local-automation-service.d.ts +25 -0
- package/dist/market-state/local-automation-service.js +119 -0
- package/dist/market-state/local-automation-service.js.map +1 -0
- package/dist/market-state/notification-delivery.d.ts +14 -0
- package/dist/market-state/notification-delivery.js +139 -0
- package/dist/market-state/notification-delivery.js.map +1 -0
- package/dist/market-state/resolve-for-mutation.d.ts +10 -0
- package/dist/market-state/resolve-for-mutation.js +15 -0
- package/dist/market-state/resolve-for-mutation.js.map +1 -0
- package/dist/market-state/resolve.d.ts +14 -0
- package/dist/market-state/resolve.js +89 -0
- package/dist/market-state/resolve.js.map +1 -0
- package/dist/market-state/service.d.ts +527 -0
- package/dist/market-state/service.js +1099 -0
- package/dist/market-state/service.js.map +1 -0
- package/dist/memory/index.d.ts +7 -7
- package/dist/memory/index.js +6 -6
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/manager.d.ts +9 -0
- package/dist/memory/manager.js +39 -22
- package/dist/memory/manager.js.map +1 -1
- package/dist/memory/retrieval.js +7 -4
- package/dist/memory/retrieval.js.map +1 -1
- package/dist/memory/sqlite.js +385 -3
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/storage.d.ts +3 -2
- package/dist/memory/storage.js +1 -2
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/tool-defaults.js +64 -28
- package/dist/memory/tool-defaults.js.map +1 -1
- package/dist/memory/types.js +4 -0
- package/dist/memory/types.js.map +1 -1
- package/dist/monitor.d.ts +2 -0
- package/dist/monitor.js +104 -0
- package/dist/monitor.js.map +1 -0
- package/dist/onboarding/connect.js +4 -6
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/credential-interceptor.js +1 -1
- package/dist/onboarding/credential-interceptor.js.map +1 -1
- package/dist/onboarding/degradation-accumulator.js +1 -3
- package/dist/onboarding/degradation-accumulator.js.map +1 -1
- package/dist/onboarding/providers.js +3 -16
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/state.js.map +1 -1
- package/dist/onboarding/tool-helpers.js +1 -1
- package/dist/onboarding/tool-helpers.js.map +1 -1
- package/dist/onboarding/tool-tags.js +6 -4
- package/dist/onboarding/tool-tags.js.map +1 -1
- package/dist/onboarding/validation.js +1 -1
- package/dist/onboarding/validation.js.map +1 -1
- package/dist/pi/opencandle-extension.d.ts +8 -0
- package/dist/pi/opencandle-extension.js +637 -59
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session.d.ts +1 -1
- package/dist/pi/session.js +3 -1
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/setup.js +17 -2
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tool-adapter.js +5 -2
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +18 -3
- package/dist/prompts/context-builder.js +117 -18
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/prompts/disclaimer.js +1 -1
- package/dist/prompts/disclaimer.js.map +1 -1
- package/dist/prompts/policy-cards.d.ts +13 -0
- package/dist/prompts/policy-cards.js +197 -0
- package/dist/prompts/policy-cards.js.map +1 -0
- package/dist/prompts/sections.d.ts +1 -1
- package/dist/prompts/sections.js +3 -3
- package/dist/prompts/sections.js.map +1 -1
- package/dist/prompts/symbol-preflight.d.ts +20 -0
- package/dist/prompts/symbol-preflight.js +49 -0
- package/dist/prompts/symbol-preflight.js.map +1 -0
- package/dist/prompts/workflow-prompts.d.ts +1 -1
- package/dist/prompts/workflow-prompts.js +209 -19
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/alpha-vantage.d.ts +1 -1
- package/dist/providers/alpha-vantage.js +49 -8
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -1
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/errors.d.ts +5 -0
- package/dist/providers/errors.js +11 -0
- package/dist/providers/errors.js.map +1 -0
- package/dist/providers/exa-search.d.ts +2 -2
- package/dist/providers/exa-search.js +19 -11
- package/dist/providers/exa-search.js.map +1 -1
- package/dist/providers/fear-greed.js +1 -1
- package/dist/providers/fear-greed.js.map +1 -1
- package/dist/providers/finnhub.js +3 -5
- package/dist/providers/finnhub.js.map +1 -1
- package/dist/providers/fred.js +2 -2
- package/dist/providers/fred.js.map +1 -1
- package/dist/providers/index.d.ts +7 -6
- package/dist/providers/index.js +6 -5
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/reddit.js +2 -2
- package/dist/providers/reddit.js.map +1 -1
- package/dist/providers/sec-edgar.d.ts +9 -1
- package/dist/providers/sec-edgar.js +181 -6
- package/dist/providers/sec-edgar.js.map +1 -1
- package/dist/providers/tradingview.d.ts +47 -0
- package/dist/providers/tradingview.js +275 -0
- package/dist/providers/tradingview.js.map +1 -0
- package/dist/providers/twitter.js +6 -8
- package/dist/providers/twitter.js.map +1 -1
- package/dist/providers/web-search.js +26 -12
- package/dist/providers/web-search.js.map +1 -1
- package/dist/providers/with-fallback.js +4 -2
- package/dist/providers/with-fallback.js.map +1 -1
- package/dist/providers/wrap-provider.d.ts +2 -3
- package/dist/providers/wrap-provider.js +14 -8
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -1
- package/dist/providers/yahoo-finance.js +226 -11
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.d.ts +9 -0
- package/dist/routing/classify-intent.js +153 -3
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/defaults.d.ts +1 -1
- package/dist/routing/defaults.js +3 -3
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/entity-extractor.d.ts +2 -0
- package/dist/routing/entity-extractor.js +377 -26
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/fund-symbols.d.ts +2 -0
- package/dist/routing/fund-symbols.js +55 -0
- package/dist/routing/fund-symbols.js.map +1 -0
- package/dist/routing/horizon.d.ts +1 -0
- package/dist/routing/horizon.js +10 -0
- package/dist/routing/horizon.js.map +1 -0
- package/dist/routing/index.d.ts +12 -6
- package/dist/routing/index.js +8 -4
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/legacy-rule-router.d.ts +9 -0
- package/dist/routing/legacy-rule-router.js +12 -0
- package/dist/routing/legacy-rule-router.js.map +1 -0
- package/dist/routing/planning.d.ts +54 -0
- package/dist/routing/planning.js +562 -0
- package/dist/routing/planning.js.map +1 -0
- package/dist/routing/route-manifest.d.ts +35 -0
- package/dist/routing/route-manifest.js +242 -0
- package/dist/routing/route-manifest.js.map +1 -0
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router-prompt.js +46 -45
- package/dist/routing/router-prompt.js.map +1 -1
- package/dist/routing/router-types.d.ts +10 -0
- package/dist/routing/router.d.ts +1 -0
- package/dist/routing/router.js +572 -13
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -1
- package/dist/routing/slot-resolver.js +45 -7
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/routing/symbol-disambiguator.d.ts +11 -0
- package/dist/routing/symbol-disambiguator.js +52 -0
- package/dist/routing/symbol-disambiguator.js.map +1 -0
- package/dist/routing/turn-context.d.ts +44 -0
- package/dist/routing/turn-context.js +45 -0
- package/dist/routing/turn-context.js.map +1 -0
- package/dist/routing/types.d.ts +15 -1
- package/dist/runtime/answer-contracts.d.ts +82 -0
- package/dist/runtime/answer-contracts.js +442 -0
- package/dist/runtime/answer-contracts.js.map +1 -0
- package/dist/runtime/artifact-contracts.d.ts +14 -0
- package/dist/runtime/artifact-contracts.js +57 -0
- package/dist/runtime/artifact-contracts.js.map +1 -0
- package/dist/runtime/planning-evidence.d.ts +99 -0
- package/dist/runtime/planning-evidence.js +466 -0
- package/dist/runtime/planning-evidence.js.map +1 -0
- package/dist/runtime/prompt-step.d.ts +1 -9
- package/dist/runtime/prompt-step.js +0 -10
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/run-context.d.ts +5 -2
- package/dist/runtime/run-context.js +8 -1
- package/dist/runtime/run-context.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +29 -3
- package/dist/runtime/session-coordinator.js +204 -31
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/session-title.d.ts +14 -0
- package/dist/runtime/session-title.js +50 -0
- package/dist/runtime/session-title.js.map +1 -0
- package/dist/runtime/tool-defaults-wrapper.js +1 -3
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
- package/dist/runtime/validation.js.map +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-runner.d.ts +3 -3
- package/dist/runtime/workflow-runner.js +1 -1
- package/dist/runtime/workflow-runner.js.map +1 -1
- package/dist/sentiment/adapters/finnhub.d.ts +1 -1
- package/dist/sentiment/adapters/finnhub.js +6 -1
- package/dist/sentiment/adapters/finnhub.js.map +1 -1
- package/dist/sentiment/adapters/reddit.d.ts +2 -2
- package/dist/sentiment/adapters/twitter.d.ts +1 -1
- package/dist/sentiment/adapters/web.d.ts +1 -1
- package/dist/sentiment/index.d.ts +9 -11
- package/dist/sentiment/index.js +9 -20
- package/dist/sentiment/index.js.map +1 -1
- package/dist/sentiment/keywords.js +26 -4
- package/dist/sentiment/keywords.js.map +1 -1
- package/dist/sentiment/pipeline.d.ts +2 -2
- package/dist/sentiment/pipeline.js +1 -1
- package/dist/sentiment/pipeline.js.map +1 -1
- package/dist/sentiment/scorer.js +1 -1
- package/dist/sentiment/store.d.ts +1 -1
- package/dist/sentiment/store.js +1 -1
- package/dist/sentiment/store.js.map +1 -1
- package/dist/sentiment/trends.d.ts +1 -1
- package/dist/sentiment/trends.js.map +1 -1
- package/dist/sentiment/types.js.map +1 -1
- package/dist/system-prompt.js +7 -3
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +7 -7
- package/dist/tool-kit.js +4 -4
- package/dist/tool-kit.js.map +1 -1
- package/dist/tools/fundamentals/company-overview.js +12 -7
- package/dist/tools/fundamentals/company-overview.js.map +1 -1
- package/dist/tools/fundamentals/comps.js +19 -10
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.js +24 -12
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.js +9 -4
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.js +9 -4
- package/dist/tools/fundamentals/financials.js.map +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +1 -0
- package/dist/tools/fundamentals/sec-filings.js +36 -4
- package/dist/tools/fundamentals/sec-filings.js.map +1 -1
- package/dist/tools/index.d.ts +23 -18
- package/dist/tools/index.js +53 -38
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.js +15 -3
- package/dist/tools/interaction/ask-user.js.map +1 -1
- package/dist/tools/interaction/twitter-login.js +13 -3
- package/dist/tools/interaction/twitter-login.js.map +1 -1
- package/dist/tools/macro/fear-greed.js +1 -1
- package/dist/tools/macro/fear-greed.js.map +1 -1
- package/dist/tools/macro/fred-data.d.ts +1 -1
- package/dist/tools/macro/fred-data.js +44 -9
- package/dist/tools/macro/fred-data.js.map +1 -1
- package/dist/tools/market/crypto-history.js +21 -3
- package/dist/tools/market/crypto-history.js.map +1 -1
- package/dist/tools/market/crypto-price.js +4 -2
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.d.ts +18 -0
- package/dist/tools/market/screen-stocks.js +252 -0
- package/dist/tools/market/screen-stocks.js.map +1 -0
- package/dist/tools/market/search-ticker.js +161 -9
- package/dist/tools/market/search-ticker.js.map +1 -1
- package/dist/tools/market/stock-history.d.ts +2 -2
- package/dist/tools/market/stock-history.js +27 -8
- package/dist/tools/market/stock-history.js.map +1 -1
- package/dist/tools/market/stock-quote.js +6 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/greeks.js +1 -2
- package/dist/tools/options/greeks.js.map +1 -1
- package/dist/tools/options/option-chain.js +27 -9
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/alerts.d.ts +15 -0
- package/dist/tools/portfolio/alerts.js +357 -0
- package/dist/tools/portfolio/alerts.js.map +1 -0
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/correlation.js +34 -14
- package/dist/tools/portfolio/correlation.js.map +1 -1
- package/dist/tools/portfolio/daily-report.d.ts +8 -0
- package/dist/tools/portfolio/daily-report.js +83 -0
- package/dist/tools/portfolio/daily-report.js.map +1 -0
- package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
- package/dist/tools/portfolio/holdings-overlap.js +112 -0
- package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
- package/dist/tools/portfolio/notifications.d.ts +7 -0
- package/dist/tools/portfolio/notifications.js +43 -0
- package/dist/tools/portfolio/notifications.js.map +1 -0
- package/dist/tools/portfolio/predictions.d.ts +12 -6
- package/dist/tools/portfolio/predictions.js +338 -88
- package/dist/tools/portfolio/predictions.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.js +46 -7
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/portfolio/tracker.d.ts +4 -3
- package/dist/tools/portfolio/tracker.js +247 -102
- package/dist/tools/portfolio/tracker.js.map +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +6 -4
- package/dist/tools/portfolio/watchlist.js +209 -101
- package/dist/tools/portfolio/watchlist.js.map +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js +24 -11
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.js +71 -14
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
- package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-trend.js +12 -2
- package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js +13 -6
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
- package/dist/tools/sentiment/untrusted-text.js +17 -0
- package/dist/tools/sentiment/untrusted-text.js.map +1 -0
- package/dist/tools/sentiment/web-search.js +37 -12
- package/dist/tools/sentiment/web-search.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.js +16 -4
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +3 -3
- package/dist/tools/technical/backtest.js +65 -44
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +24 -8
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/market.d.ts +1 -0
- package/dist/types/options.d.ts +10 -0
- package/dist/types/portfolio.d.ts +41 -4
- package/dist/workflows/compare-assets.d.ts +0 -3
- package/dist/workflows/compare-assets.js +55 -10
- package/dist/workflows/compare-assets.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -4
- package/dist/workflows/index.js +3 -3
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/options-screener.d.ts +0 -3
- package/dist/workflows/options-screener.js +88 -14
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.d.ts +0 -3
- package/dist/workflows/portfolio-builder.js +7 -11
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/ask-user-bridge.ts +82 -0
- package/gui/server/automation-heartbeat.ts +97 -0
- package/gui/server/background-quotes.ts +97 -1
- package/gui/server/chat-event-adapter.ts +32 -10
- package/gui/server/chat-run-session.ts +16 -0
- package/gui/server/gui-session-manager.ts +5 -0
- package/gui/server/invoke-tool.ts +144 -1
- package/gui/server/live-chat-event-adapter.ts +21 -6
- package/gui/server/market-state-api.ts +315 -0
- package/gui/server/model-setup.ts +149 -2
- package/gui/server/private-api-access.ts +62 -0
- package/gui/server/projector.ts +58 -11
- package/gui/server/prompt-observation.ts +58 -0
- package/gui/server/quote-snapshot-store.ts +50 -0
- package/gui/server/server.ts +236 -376
- package/gui/server/session-actions.ts +186 -1
- package/gui/server/session-entry-wait.ts +81 -0
- package/gui/server/shutdown.ts +47 -0
- package/gui/server/tool-invoke-ack.ts +49 -0
- package/gui/server/tool-metadata.ts +23 -10
- package/gui/server/websocket.ts +13 -3
- package/gui/server/writer-lock.ts +6 -2
- package/gui/server/ws-hub.ts +292 -0
- package/gui/shared/chat-events.ts +16 -1
- package/gui/shared/event-reducer.ts +24 -6
- package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +1 -0
- package/gui/web/dist/assets/index-2KZtKBmu.css +1 -0
- package/gui/web/dist/assets/index-CveNgtDg.js +69 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +22 -12
- package/src/analysts/contracts.ts +10 -23
- package/src/analysts/orchestrator.ts +8 -43
- package/src/cli.ts +37 -13
- package/src/config.ts +99 -7
- package/src/index.ts +1 -1
- package/src/infra/browser.ts +4 -2
- package/src/infra/cache.ts +41 -30
- package/src/infra/http-client.ts +72 -6
- package/src/infra/index.ts +7 -10
- package/src/infra/native-dependencies.ts +8 -3
- package/src/infra/node-version.ts +3 -1
- package/src/infra/opencandle-paths.ts +3 -14
- package/src/infra/rate-limiter.ts +32 -20
- package/src/market-state/alert-conditions.ts +82 -0
- package/src/market-state/alert-runner.ts +863 -0
- package/src/market-state/daily-report.ts +247 -0
- package/src/market-state/local-automation-service.ts +162 -0
- package/src/market-state/notification-delivery.ts +158 -0
- package/src/market-state/resolve-for-mutation.ts +24 -0
- package/src/market-state/resolve.ts +112 -0
- package/src/market-state/service.ts +2344 -0
- package/src/memory/index.ts +7 -7
- package/src/memory/manager.ts +57 -26
- package/src/memory/retrieval.ts +8 -7
- package/src/memory/sqlite.ts +407 -6
- package/src/memory/storage.ts +8 -17
- package/src/memory/tool-defaults.ts +60 -39
- package/src/memory/types.ts +7 -3
- package/src/monitor.ts +121 -0
- package/src/onboarding/connect.ts +10 -33
- package/src/onboarding/credential-interceptor.ts +3 -15
- package/src/onboarding/degradation-accumulator.ts +1 -3
- package/src/onboarding/providers.ts +9 -40
- package/src/onboarding/state.ts +4 -15
- package/src/onboarding/tool-helpers.ts +2 -9
- package/src/onboarding/tool-tags.ts +6 -6
- package/src/onboarding/validation.ts +14 -20
- package/src/pi/opencandle-extension.ts +795 -120
- package/src/pi/session.ts +7 -5
- package/src/pi/setup.ts +61 -33
- package/src/pi/tool-adapter.ts +5 -2
- package/src/prompts/context-builder.ts +143 -21
- package/src/prompts/disclaimer.ts +1 -1
- package/src/prompts/policy-cards.ts +220 -0
- package/src/prompts/sections.ts +4 -4
- package/src/prompts/symbol-preflight.ts +80 -0
- package/src/prompts/workflow-prompts.ts +231 -28
- package/src/providers/alpha-vantage.ts +82 -40
- package/src/providers/coingecko.ts +2 -5
- package/src/providers/errors.ts +9 -0
- package/src/providers/exa-search.ts +24 -22
- package/src/providers/fear-greed.ts +1 -1
- package/src/providers/finnhub.ts +7 -6
- package/src/providers/fred.ts +3 -3
- package/src/providers/index.ts +14 -6
- package/src/providers/reddit.ts +17 -6
- package/src/providers/sec-edgar.ts +235 -5
- package/src/providers/tradingview.ts +399 -0
- package/src/providers/twitter.ts +6 -8
- package/src/providers/web-search.ts +30 -20
- package/src/providers/with-fallback.ts +8 -7
- package/src/providers/wrap-provider.ts +15 -10
- package/src/providers/yahoo-finance.ts +292 -20
- package/src/routing/classify-intent.ts +186 -4
- package/src/routing/defaults.ts +4 -4
- package/src/routing/entity-extractor.ts +428 -28
- package/src/routing/fund-symbols.ts +58 -0
- package/src/routing/horizon.ts +7 -0
- package/src/routing/index.ts +60 -16
- package/src/routing/legacy-rule-router.ts +13 -0
- package/src/routing/planning.ts +823 -0
- package/src/routing/route-manifest.ts +309 -0
- package/src/routing/router-llm-client.ts +4 -4
- package/src/routing/router-prompt.ts +52 -52
- package/src/routing/router-types.ts +18 -0
- package/src/routing/router.ts +717 -20
- package/src/routing/slot-resolver.ts +75 -14
- package/src/routing/symbol-disambiguator.ts +72 -0
- package/src/routing/turn-context.ts +108 -0
- package/src/routing/types.ts +15 -1
- package/src/runtime/answer-contracts.ts +672 -0
- package/src/runtime/artifact-contracts.ts +77 -0
- package/src/runtime/planning-evidence.ts +682 -0
- package/src/runtime/prompt-step.ts +1 -16
- package/src/runtime/run-context.ts +12 -2
- package/src/runtime/session-coordinator.ts +297 -56
- package/src/runtime/session-title.ts +60 -0
- package/src/runtime/tool-defaults-wrapper.ts +1 -3
- package/src/runtime/validation.ts +1 -4
- package/src/runtime/workflow-events.ts +7 -7
- package/src/runtime/workflow-runner.ts +5 -11
- package/src/sentiment/adapters/finnhub.ts +7 -2
- package/src/sentiment/adapters/reddit.ts +2 -2
- package/src/sentiment/adapters/twitter.ts +1 -1
- package/src/sentiment/adapters/web.ts +1 -1
- package/src/sentiment/index.ts +16 -26
- package/src/sentiment/keywords.ts +26 -4
- package/src/sentiment/pipeline.ts +15 -4
- package/src/sentiment/scorer.ts +1 -1
- package/src/sentiment/store.ts +2 -2
- package/src/sentiment/trends.ts +9 -3
- package/src/sentiment/types.ts +5 -4
- package/src/system-prompt.ts +7 -3
- package/src/tool-kit.ts +10 -9
- package/src/tools/fundamentals/company-overview.ts +20 -10
- package/src/tools/fundamentals/comps.ts +69 -56
- package/src/tools/fundamentals/dcf.ts +146 -96
- package/src/tools/fundamentals/earnings.ts +17 -7
- package/src/tools/fundamentals/financials.ts +17 -8
- package/src/tools/fundamentals/sec-filings.ts +52 -8
- package/src/tools/index.ts +53 -38
- package/src/tools/interaction/ask-user.ts +22 -10
- package/src/tools/interaction/twitter-login.ts +17 -5
- package/src/tools/macro/fear-greed.ts +2 -2
- package/src/tools/macro/fred-data.ts +80 -42
- package/src/tools/market/crypto-history.ts +25 -4
- package/src/tools/market/crypto-price.ts +7 -7
- package/src/tools/market/screen-stocks.ts +279 -0
- package/src/tools/market/search-ticker.ts +219 -18
- package/src/tools/market/stock-history.ts +38 -13
- package/src/tools/market/stock-quote.ts +11 -8
- package/src/tools/options/greeks.ts +5 -6
- package/src/tools/options/option-chain.ts +47 -18
- package/src/tools/portfolio/alerts.ts +457 -0
- package/src/tools/portfolio/correlation.ts +48 -21
- package/src/tools/portfolio/daily-report.ts +101 -0
- package/src/tools/portfolio/holdings-overlap.ts +139 -0
- package/src/tools/portfolio/notifications.ts +45 -0
- package/src/tools/portfolio/predictions.ts +407 -107
- package/src/tools/portfolio/risk-analysis.ts +47 -8
- package/src/tools/portfolio/tracker.ts +271 -110
- package/src/tools/portfolio/watchlist.ts +251 -116
- package/src/tools/sentiment/reddit-sentiment.ts +51 -25
- package/src/tools/sentiment/sentiment-summary.ts +116 -35
- package/src/tools/sentiment/sentiment-trend.ts +24 -7
- package/src/tools/sentiment/twitter-sentiment.ts +23 -16
- package/src/tools/sentiment/untrusted-text.ts +21 -0
- package/src/tools/sentiment/web-search.ts +52 -16
- package/src/tools/sentiment/web-sentiment.ts +27 -11
- package/src/tools/technical/backtest.ts +78 -47
- package/src/tools/technical/indicators.ts +40 -17
- package/src/types/index.ts +8 -3
- package/src/types/market.ts +1 -0
- package/src/types/options.ts +17 -0
- package/src/types/portfolio.ts +46 -4
- package/src/types/sentiment.ts +2 -2
- package/src/workflows/compare-assets.ts +67 -19
- package/src/workflows/index.ts +3 -4
- package/src/workflows/options-screener.ts +98 -22
- package/src/workflows/portfolio-builder.ts +40 -29
- package/dist/runtime/index.d.ts +0 -16
- package/dist/runtime/index.js +0 -10
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/provider-ids.d.ts +0 -14
- package/dist/runtime/provider-ids.js +0 -14
- package/dist/runtime/provider-ids.js.map +0 -1
- package/dist/workflows/types.d.ts +0 -4
- package/dist/workflows/types.js +0 -2
- package/dist/workflows/types.js.map +0 -1
- package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +0 -1
- package/gui/web/dist/assets/index-DBrWq43L.css +0 -1
- package/gui/web/dist/assets/index-RflHaj0y.js +0 -67
- package/src/runtime/index.ts +0 -55
- package/src/runtime/provider-ids.ts +0 -15
- 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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import {
|
|
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(
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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, {
|
|
465
|
-
|
|
466
|
-
|
|
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
|
|
602
|
+
return dispatched || undefined;
|
|
477
603
|
}
|
|
478
604
|
|
|
479
|
-
// --- rules mode (
|
|
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
|
-
|
|
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(
|
|
491
|
-
|
|
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.
|
|
494
|
-
|
|
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(
|
|
501
|
-
|
|
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.
|
|
504
|
-
|
|
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 (
|
|
732
|
+
if (
|
|
733
|
+
classification.workflow === "compare_assets" &&
|
|
734
|
+
classification.entities.symbols.length >= 2
|
|
735
|
+
) {
|
|
509
736
|
const resolution: SlotResolution<CompareAssetsSlots> = {
|
|
510
|
-
resolved: {
|
|
511
|
-
|
|
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(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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<
|
|
829
|
+
): Promise<{ action: "transform"; text: string } | false> {
|
|
534
830
|
const storage = coordinator.getStorage();
|
|
535
|
-
const { profileSnapshot, recentWorkflowRuns, priorTurns } =
|
|
536
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
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 =
|
|
954
|
+
const resolution = withRouterSlotSources(
|
|
955
|
+
resolvePortfolioSlots(entities, workflowPrefs),
|
|
956
|
+
output,
|
|
957
|
+
);
|
|
623
958
|
coordinator.recordWorkflowRun(
|
|
624
959
|
"portfolio_builder",
|
|
625
|
-
|
|
960
|
+
entities,
|
|
626
961
|
resolution.resolved,
|
|
627
962
|
resolution.defaultsUsed,
|
|
628
|
-
|
|
963
|
+
output.routeKind,
|
|
629
964
|
);
|
|
630
965
|
pi.appendEntry("opencandle-workflow", {
|
|
631
966
|
workflow: "portfolio_builder",
|
|
632
|
-
entities
|
|
967
|
+
entities,
|
|
633
968
|
resolved: resolution.resolved,
|
|
634
969
|
});
|
|
635
970
|
const definition = buildPortfolioWorkflowDefinition(resolution);
|
|
636
|
-
coordinator.
|
|
637
|
-
|
|
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 =
|
|
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
|
-
|
|
985
|
+
entities,
|
|
647
986
|
resolution.resolved,
|
|
648
987
|
resolution.defaultsUsed,
|
|
649
|
-
|
|
988
|
+
output.routeKind,
|
|
650
989
|
);
|
|
651
990
|
pi.appendEntry("opencandle-workflow", {
|
|
652
991
|
workflow: "options_screener",
|
|
653
|
-
entities
|
|
992
|
+
entities,
|
|
654
993
|
resolved: resolution.resolved,
|
|
655
994
|
});
|
|
656
995
|
const definition = buildOptionsScreenerWorkflowDefinition(resolution);
|
|
657
|
-
coordinator.
|
|
658
|
-
|
|
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" &&
|
|
1002
|
+
if (workflow === "compare_assets" && entities.symbols.length >= 2) {
|
|
663
1003
|
const resolution: SlotResolution<CompareAssetsSlots> = {
|
|
664
|
-
resolved: {
|
|
665
|
-
|
|
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
|
-
|
|
1025
|
+
entities,
|
|
672
1026
|
resolution.resolved,
|
|
673
1027
|
[],
|
|
674
|
-
|
|
1028
|
+
output.routeKind,
|
|
675
1029
|
);
|
|
676
1030
|
pi.appendEntry("opencandle-workflow", {
|
|
677
1031
|
workflow: "compare_assets",
|
|
678
|
-
symbols:
|
|
1032
|
+
symbols: entities.symbols,
|
|
679
1033
|
});
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
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: ${
|
|
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(
|
|
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
|
+
}
|