opencandle 0.10.0 → 0.11.1

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 (223) hide show
  1. package/README.md +12 -9
  2. package/dist/analysts/orchestrator.d.ts +3 -4
  3. package/dist/analysts/orchestrator.js +34 -44
  4. package/dist/analysts/orchestrator.js.map +1 -1
  5. package/dist/cli-main.js +242 -9
  6. package/dist/cli-main.js.map +1 -1
  7. package/dist/config.d.ts +4 -9
  8. package/dist/config.js +7 -10
  9. package/dist/config.js.map +1 -1
  10. package/dist/doctor/report.js +1 -1
  11. package/dist/doctor/report.js.map +1 -1
  12. package/dist/infra/cache.d.ts +6 -0
  13. package/dist/infra/cache.js +16 -6
  14. package/dist/infra/cache.js.map +1 -1
  15. package/dist/infra/freshness.d.ts +21 -0
  16. package/dist/infra/freshness.js +119 -0
  17. package/dist/infra/freshness.js.map +1 -0
  18. package/dist/infra/market-calendar.d.ts +14 -0
  19. package/dist/infra/market-calendar.js +137 -0
  20. package/dist/infra/market-calendar.js.map +1 -0
  21. package/dist/infra/rate-limiter.js +1 -0
  22. package/dist/infra/rate-limiter.js.map +1 -1
  23. package/dist/market-state/daily-report.d.ts +1 -1
  24. package/dist/market-state/daily-report.js +5 -16
  25. package/dist/market-state/daily-report.js.map +1 -1
  26. package/dist/market-state/service.d.ts +0 -35
  27. package/dist/market-state/service.js +0 -63
  28. package/dist/market-state/service.js.map +1 -1
  29. package/dist/market-state/summaries.d.ts +8 -0
  30. package/dist/market-state/summaries.js +70 -0
  31. package/dist/market-state/summaries.js.map +1 -0
  32. package/dist/memory/sqlite.js +20 -19
  33. package/dist/memory/sqlite.js.map +1 -1
  34. package/dist/onboarding/providers.d.ts +25 -1
  35. package/dist/onboarding/providers.js +26 -0
  36. package/dist/onboarding/providers.js.map +1 -1
  37. package/dist/pi/opencandle-extension.js +57 -226
  38. package/dist/pi/opencandle-extension.js.map +1 -1
  39. package/dist/pi/session-action-dedupe.d.ts +6 -0
  40. package/dist/pi/session-action-dedupe.js +126 -0
  41. package/dist/pi/session-action-dedupe.js.map +1 -0
  42. package/dist/pi/session-writer-lock.d.ts +26 -2
  43. package/dist/pi/session-writer-lock.js +230 -18
  44. package/dist/pi/session-writer-lock.js.map +1 -1
  45. package/dist/pi/setup.js +5 -5
  46. package/dist/pi/setup.js.map +1 -1
  47. package/dist/pi/tui-session-coordinator.d.ts +15 -0
  48. package/dist/pi/tui-session-coordinator.js +283 -0
  49. package/dist/pi/tui-session-coordinator.js.map +1 -0
  50. package/dist/prompts/context-builder.js +1 -1
  51. package/dist/prompts/policy-cards.js +1 -1
  52. package/dist/prompts/policy-cards.js.map +1 -1
  53. package/dist/providers/alpha-vantage.js +6 -0
  54. package/dist/providers/alpha-vantage.js.map +1 -1
  55. package/dist/providers/coingecko.js +1 -0
  56. package/dist/providers/coingecko.js.map +1 -1
  57. package/dist/providers/polymarket.d.ts +2 -0
  58. package/dist/providers/polymarket.js +163 -0
  59. package/dist/providers/polymarket.js.map +1 -0
  60. package/dist/providers/wrap-provider.js +5 -3
  61. package/dist/providers/wrap-provider.js.map +1 -1
  62. package/dist/providers/yahoo-finance.d.ts +3 -0
  63. package/dist/providers/yahoo-finance.js +206 -10
  64. package/dist/providers/yahoo-finance.js.map +1 -1
  65. package/dist/routing/classify-intent.js +4 -5
  66. package/dist/routing/classify-intent.js.map +1 -1
  67. package/dist/routing/entity-extractor.js +16 -0
  68. package/dist/routing/entity-extractor.js.map +1 -1
  69. package/dist/routing/route-manifest.js +1 -2
  70. package/dist/routing/route-manifest.js.map +1 -1
  71. package/dist/routing/router-llm-client.js +18 -3
  72. package/dist/routing/router-llm-client.js.map +1 -1
  73. package/dist/routing/router-prompt.js +1 -1
  74. package/dist/routing/router.d.ts +1 -1
  75. package/dist/routing/router.js +309 -9
  76. package/dist/routing/router.js.map +1 -1
  77. package/dist/routing/slot-resolver.d.ts +1 -0
  78. package/dist/routing/slot-resolver.js +1 -1
  79. package/dist/routing/slot-resolver.js.map +1 -1
  80. package/dist/runtime/evidence.d.ts +1 -0
  81. package/dist/runtime/evidence.js.map +1 -1
  82. package/dist/runtime/numeric-claims.d.ts +23 -0
  83. package/dist/runtime/numeric-claims.js +100 -0
  84. package/dist/runtime/numeric-claims.js.map +1 -0
  85. package/dist/runtime/planning-evidence.js +1 -76
  86. package/dist/runtime/planning-evidence.js.map +1 -1
  87. package/dist/runtime/prompt-step.d.ts +10 -1
  88. package/dist/runtime/prompt-step.js +116 -2
  89. package/dist/runtime/prompt-step.js.map +1 -1
  90. package/dist/runtime/session-coordinator.d.ts +11 -1
  91. package/dist/runtime/session-coordinator.js +354 -62
  92. package/dist/runtime/session-coordinator.js.map +1 -1
  93. package/dist/runtime/validation.d.ts +3 -1
  94. package/dist/runtime/workflow-events.d.ts +1 -1
  95. package/dist/runtime/workflow-events.js.map +1 -1
  96. package/dist/runtime/workflow-types.d.ts +3 -0
  97. package/dist/runtime/workflow-types.js.map +1 -1
  98. package/dist/system-prompt.js +1 -1
  99. package/dist/tools/fundamentals/comps.d.ts +1 -0
  100. package/dist/tools/fundamentals/comps.js +20 -9
  101. package/dist/tools/fundamentals/comps.js.map +1 -1
  102. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  103. package/dist/tools/fundamentals/dcf.js +190 -76
  104. package/dist/tools/fundamentals/dcf.js.map +1 -1
  105. package/dist/tools/index.d.ts +1 -1
  106. package/dist/tools/index.js +3 -3
  107. package/dist/tools/index.js.map +1 -1
  108. package/dist/tools/macro/event-probabilities.d.ts +8 -0
  109. package/dist/tools/macro/event-probabilities.js +121 -0
  110. package/dist/tools/macro/event-probabilities.js.map +1 -0
  111. package/dist/tools/market/crypto-price.d.ts +4 -1
  112. package/dist/tools/market/crypto-price.js +10 -1
  113. package/dist/tools/market/crypto-price.js.map +1 -1
  114. package/dist/tools/market/screen-stocks.js +14 -3
  115. package/dist/tools/market/screen-stocks.js.map +1 -1
  116. package/dist/tools/market/stock-quote.d.ts +4 -1
  117. package/dist/tools/market/stock-quote.js +9 -4
  118. package/dist/tools/market/stock-quote.js.map +1 -1
  119. package/dist/tools/options/option-chain.d.ts +4 -1
  120. package/dist/tools/options/option-chain.js +12 -1
  121. package/dist/tools/options/option-chain.js.map +1 -1
  122. package/dist/tools/portfolio/daily-report.js +10 -4
  123. package/dist/tools/portfolio/daily-report.js.map +1 -1
  124. package/dist/tools/portfolio/risk-analysis.js +1 -1
  125. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  126. package/dist/tools/technical/backtest.d.ts +23 -5
  127. package/dist/tools/technical/backtest.js +131 -94
  128. package/dist/tools/technical/backtest.js.map +1 -1
  129. package/dist/tools/technical/indicators.js +1 -1
  130. package/dist/tools/technical/indicators.js.map +1 -1
  131. package/dist/types/fundamentals.d.ts +1 -0
  132. package/dist/types/index.d.ts +1 -0
  133. package/dist/types/market.d.ts +2 -0
  134. package/dist/types/options.d.ts +1 -0
  135. package/dist/types/prediction-markets.d.ts +13 -0
  136. package/dist/types/prediction-markets.js +2 -0
  137. package/dist/types/prediction-markets.js.map +1 -0
  138. package/gui/server/chat-event-adapter.ts +89 -5
  139. package/gui/server/http-routes.ts +661 -37
  140. package/gui/server/invoke-tool.ts +165 -16
  141. package/gui/server/live-chat-event-adapter.ts +37 -2
  142. package/gui/server/local-session-coordinator.ts +97 -0
  143. package/gui/server/market-state-api.ts +64 -48
  144. package/gui/server/projector.ts +132 -3
  145. package/gui/server/server.ts +51 -6
  146. package/gui/server/session-actions.ts +146 -45
  147. package/gui/server/session-entry-wait.ts +22 -3
  148. package/gui/server/tool-metadata.ts +3 -1
  149. package/gui/server/ws-hub.ts +77 -10
  150. package/gui/shared/chat-events.ts +10 -1
  151. package/gui/shared/event-reducer.ts +1 -0
  152. package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
  153. package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
  154. package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
  155. package/gui/web/dist/index.html +2 -2
  156. package/package.json +13 -15
  157. package/src/analysts/orchestrator.ts +35 -56
  158. package/src/cli-main.ts +253 -13
  159. package/src/config.ts +12 -20
  160. package/src/doctor/report.ts +1 -1
  161. package/src/infra/cache.ts +24 -11
  162. package/src/infra/freshness.ts +165 -0
  163. package/src/infra/market-calendar.ts +193 -0
  164. package/src/infra/rate-limiter.ts +1 -0
  165. package/src/market-state/daily-report.ts +6 -16
  166. package/src/market-state/service.ts +0 -136
  167. package/src/market-state/summaries.ts +75 -0
  168. package/src/memory/sqlite.ts +23 -19
  169. package/src/onboarding/providers.ts +29 -1
  170. package/src/pi/opencandle-extension.ts +67 -270
  171. package/src/pi/session-action-dedupe.ts +155 -0
  172. package/src/pi/session-writer-lock.ts +290 -20
  173. package/src/pi/setup.ts +6 -6
  174. package/src/pi/tui-session-coordinator.ts +351 -0
  175. package/src/prompts/context-builder.ts +1 -1
  176. package/src/prompts/policy-cards.ts +1 -1
  177. package/src/providers/alpha-vantage.ts +6 -0
  178. package/src/providers/coingecko.ts +2 -0
  179. package/src/providers/polymarket.ts +214 -0
  180. package/src/providers/wrap-provider.ts +5 -3
  181. package/src/providers/yahoo-finance.ts +339 -27
  182. package/src/routing/classify-intent.ts +4 -5
  183. package/src/routing/entity-extractor.ts +17 -0
  184. package/src/routing/route-manifest.ts +1 -2
  185. package/src/routing/router-llm-client.ts +31 -19
  186. package/src/routing/router-prompt.ts +1 -1
  187. package/src/routing/router.ts +379 -8
  188. package/src/routing/slot-resolver.ts +1 -1
  189. package/src/runtime/evidence.ts +1 -0
  190. package/src/runtime/numeric-claims.ts +108 -0
  191. package/src/runtime/planning-evidence.ts +6 -91
  192. package/src/runtime/prompt-step.ts +127 -2
  193. package/src/runtime/session-coordinator.ts +464 -74
  194. package/src/runtime/validation.ts +1 -1
  195. package/src/runtime/workflow-events.ts +2 -0
  196. package/src/runtime/workflow-types.ts +3 -0
  197. package/src/system-prompt.ts +1 -1
  198. package/src/tools/AGENTS.md +1 -1
  199. package/src/tools/fundamentals/comps.ts +25 -14
  200. package/src/tools/fundamentals/dcf.ts +217 -87
  201. package/src/tools/index.ts +3 -3
  202. package/src/tools/macro/event-probabilities.ts +141 -0
  203. package/src/tools/market/crypto-price.ts +14 -2
  204. package/src/tools/market/screen-stocks.ts +14 -3
  205. package/src/tools/market/stock-quote.ts +49 -43
  206. package/src/tools/options/option-chain.ts +16 -2
  207. package/src/tools/portfolio/daily-report.ts +10 -4
  208. package/src/tools/portfolio/risk-analysis.ts +1 -1
  209. package/src/tools/technical/backtest.ts +167 -108
  210. package/src/tools/technical/indicators.ts +1 -1
  211. package/src/types/fundamentals.ts +1 -0
  212. package/src/types/index.ts +1 -0
  213. package/src/types/market.ts +2 -0
  214. package/src/types/options.ts +1 -0
  215. package/src/types/prediction-markets.ts +13 -0
  216. package/dist/tools/portfolio/predictions.d.ts +0 -55
  217. package/dist/tools/portfolio/predictions.js +0 -422
  218. package/dist/tools/portfolio/predictions.js.map +0 -1
  219. package/gui/server/chat-run-session.ts +0 -16
  220. package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
  221. package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
  222. package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
  223. package/src/tools/portfolio/predictions.ts +0 -553
@@ -4,6 +4,16 @@ import type {
4
4
  ExtensionContext,
5
5
  SessionEntry,
6
6
  } from "@earendil-works/pi-coding-agent";
7
+ import { isAnalystSplit, parseAnalystOutput, parseDebateOutput } from "../analysts/contracts.js";
8
+ import { buildAnalystVoteTallyBlock } from "../analysts/orchestrator.js";
9
+ import type { FreshnessStamp } from "../infra/freshness.js";
10
+ import { MarketStateService } from "../market-state/service.js";
11
+ import {
12
+ formatJsonSummary,
13
+ formatLatestReportSummary,
14
+ formatPortfolioSummary,
15
+ formatWatchlistSummary,
16
+ } from "../market-state/summaries.js";
7
17
  import { getAllDefaults, initDefaultDatabase, MemoryStorage } from "../memory/index.js";
8
18
 
9
19
  /**
@@ -15,7 +25,6 @@ import { getAllDefaults, initDefaultDatabase, MemoryStorage } from "../memory/in
15
25
  type ReadonlySessionManager = ExtensionContext["sessionManager"];
16
26
 
17
27
  import type Database from "better-sqlite3";
18
- import { MarketStateService } from "../market-state/service.js";
19
28
  import type { FilteredMemoryEntry } from "../memory/manager.js";
20
29
  import { MemoryManager } from "../memory/manager.js";
21
30
  import { extractPreferences } from "../memory/preference-extractor.js";
@@ -26,12 +35,21 @@ import type { SymbolValidationCache } from "../prompts/symbol-preflight.js";
26
35
  import type { RouterRouteKind } from "../routing/router-types.js";
27
36
  import type { ResolvedTurnContext } from "../routing/turn-context.js";
28
37
  import { getAddonToolDescriptions } from "../tool-kit.js";
38
+ import type { EvidenceRecord } from "./evidence.js";
39
+ import { collectToolNumbers, extractNumericClaims } from "./numeric-claims.js";
29
40
  import type { WorkflowDefinition } from "./prompt-step.js";
30
- import { promptStepOutput, toStepDefinitions } from "./prompt-step.js";
41
+ import {
42
+ captureToolEvidence,
43
+ extractAssistantText,
44
+ promptStepOutput,
45
+ toStepDefinitions,
46
+ } from "./prompt-step.js";
31
47
  import { ProviderTracker } from "./provider-tracker.js";
32
48
  import { clearRunContext, type RunContextToken, setRunContext } from "./run-context.js";
49
+ import { checkNumberMatch } from "./validation.js";
33
50
  import { WorkflowEventLogger } from "./workflow-events.js";
34
51
  import { WorkflowRunner } from "./workflow-runner.js";
52
+ import type { AnalystOutput, StepOutput, WorkflowRun } from "./workflow-types.js";
35
53
 
36
54
  const PROMPT_SETTLE_POLL_MS = 25;
37
55
  const IMMEDIATE_IDLE_GRACE_MS = 100;
@@ -41,6 +59,12 @@ interface ActiveWorkflowRunRef {
41
59
  contextToken: RunContextToken;
42
60
  }
43
61
 
62
+ interface ActiveStepCapture {
63
+ evidence: EvidenceRecord[];
64
+ pendingTools: Map<string, { tool: string; args: unknown; startedAt: string }>;
65
+ rawText: string;
66
+ }
67
+
44
68
  function parseMaybeJson(raw: unknown): Record<string, unknown> | undefined {
45
69
  if (typeof raw !== "string" || raw.length === 0) return undefined;
46
70
  try {
@@ -59,6 +83,7 @@ type QueueContext =
59
83
  isIdle(): boolean;
60
84
  hasPendingMessages?(): boolean;
61
85
  ui?: { notify(message: string, level?: string): void };
86
+ sessionManager?: { getEntries(): SessionEntry[] };
62
87
  };
63
88
 
64
89
  function hasPendingMessages(ctx: QueueContext): boolean {
@@ -69,6 +94,11 @@ function isReadyForNextPrompt(ctx: QueueContext): boolean {
69
94
  return ctx.isIdle() && !hasPendingMessages(ctx);
70
95
  }
71
96
 
97
+ function readSessionEntries(ctx: QueueContext): SessionEntry[] {
98
+ const manager = "sessionManager" in ctx ? ctx.sessionManager : undefined;
99
+ return manager?.getEntries?.() ?? [];
100
+ }
101
+
72
102
  function sleep(ms: number): Promise<void> {
73
103
  return new Promise((resolve) => setTimeout(resolve, ms));
74
104
  }
@@ -119,6 +149,8 @@ export class SessionCoordinator {
119
149
  private providerTracker: ProviderTracker;
120
150
  private activeWorkflowRunRef: ActiveWorkflowRunRef | null = null;
121
151
  private activeWorkflowType: string | undefined;
152
+ private activeStepCapture: ActiveStepCapture | null = null;
153
+ private workflowEventCaptureInstalled = false;
122
154
  private tickerValidationCache: SymbolValidationCache = new Map();
123
155
  private sessionId = "unknown";
124
156
 
@@ -416,6 +448,7 @@ export class SessionCoordinator {
416
448
  firstPromptMode: "send" | "transform",
417
449
  ): void {
418
450
  if (definition.steps.length === 0) return;
451
+ this.installWorkflowEventCapture(pi);
419
452
 
420
453
  const runner = this.runner;
421
454
  if (this.activeWorkflowRunRef) {
@@ -444,26 +477,101 @@ export class SessionCoordinator {
444
477
  // Start the runner in the background for state tracking
445
478
  const stepDefs = toStepDefinitions(definition.steps);
446
479
  void runner
447
- .start(definition.workflowType, stepDefs, async (step, stepIndex) => {
448
- // First step was already sent above — just wait for settlement
449
- if (stepIndex > 0) {
450
- const settled = await waitForPromptSettlement(ctx, () => runRef.active);
451
- if (!settled || !runRef.active) {
452
- throw new Error("run_cancelled");
480
+ .start(
481
+ definition.workflowType,
482
+ stepDefs,
483
+ async (step, stepIndex, _priorEvidence, context) => {
484
+ let entriesBeforeStep = readSessionEntries(ctx).length;
485
+ const eventCapture = this.startStepCapture();
486
+
487
+ // First step was already sent above — later steps are sent here.
488
+ if (stepIndex > 0) {
489
+ const settled = await waitForPromptSettlement(ctx, () => runRef.active);
490
+ if (!settled || !runRef.active) {
491
+ throw new Error("run_cancelled");
492
+ }
493
+ if (shouldSkipRebuttal(runner.getActiveRun(), step.stepType)) {
494
+ this.appendWorkflowEvent(pi, "step_skipped", {
495
+ stepType: step.stepType,
496
+ reason: "analyst_consensus",
497
+ });
498
+ throw new Error("analyst_consensus");
499
+ }
500
+ entriesBeforeStep = readSessionEntries(ctx).length;
501
+ const prompt = this.prepareWorkflowPrompt(
502
+ pi,
503
+ definition.steps[stepIndex].prompt,
504
+ step.stepType,
505
+ runner.getActiveRun(),
506
+ );
507
+ pi.sendUserMessage(prompt);
453
508
  }
454
- pi.sendUserMessage(definition.steps[stepIndex].prompt);
455
- } else {
456
- // For the first step, just wait for it to settle
509
+
457
510
  const settled = await waitForPromptSettlement(ctx, () => runRef.active, {
458
- requireActivity: firstPromptMode === "transform",
511
+ requireActivity: stepIndex === 0 && firstPromptMode === "transform",
459
512
  });
460
513
  if (!settled || !runRef.active) {
461
514
  throw new Error("run_cancelled");
462
515
  }
463
- }
464
- return promptStepOutput(stepIndex, step.stepType);
465
- })
516
+
517
+ let stepEntries = readSessionEntries(ctx).slice(entriesBeforeStep);
518
+ let rawText = capturedText(eventCapture, stepEntries);
519
+ let output = promptStepOutput(stepIndex, step.stepType, {
520
+ evidence: capturedEvidence(eventCapture, stepEntries),
521
+ rawText,
522
+ });
523
+
524
+ if (
525
+ isStructuredAnalystStep(step.stepType) &&
526
+ !hasStructuredContract(step.stepType, rawText)
527
+ ) {
528
+ pi.sendUserMessage(
529
+ "Please revise your previous response to include the exact required final output format from the stage prompt. Do not add new tool calls.",
530
+ );
531
+ const retrySettled = await waitForPromptSettlement(ctx, () => runRef.active);
532
+ if (!retrySettled || !runRef.active) {
533
+ throw new Error("run_cancelled");
534
+ }
535
+ stepEntries = readSessionEntries(ctx).slice(entriesBeforeStep);
536
+ rawText = capturedText(eventCapture, stepEntries);
537
+ output = promptStepOutput(stepIndex, step.stepType, {
538
+ evidence: capturedEvidence(eventCapture, stepEntries),
539
+ rawText,
540
+ });
541
+ }
542
+
543
+ for (const record of output.evidence) {
544
+ const value = isPlainObject(record.value) ? record.value : {};
545
+ this.eventLogger?.log(context.runId, stepIndex, "tool_called", {
546
+ stepType: step.stepType,
547
+ tool: value.tool,
548
+ args: value.args,
549
+ resultDigest: value.resultDigest,
550
+ });
551
+ }
552
+
553
+ if (this.activeStepCapture === eventCapture) {
554
+ this.activeStepCapture = null;
555
+ }
556
+
557
+ const structuredOutput = attachStructuredOutput(pi, output);
558
+ if (step.stepType === "synthesis") {
559
+ this.emitSynthesisValidation(
560
+ pi,
561
+ context.runId,
562
+ stepIndex,
563
+ runner.getActiveRun(),
564
+ structuredOutput,
565
+ );
566
+ }
567
+
568
+ return structuredOutput;
569
+ },
570
+ )
466
571
  .finally(() => {
572
+ if (this.activeWorkflowRunRef === runRef) {
573
+ this.activeStepCapture = null;
574
+ }
467
575
  clearRunContext(runRef.contextToken);
468
576
  if (this.activeWorkflowRunRef === runRef) {
469
577
  this.activeWorkflowRunRef = null;
@@ -479,8 +587,261 @@ export class SessionCoordinator {
479
587
  clearRunContext(activeRef.contextToken);
480
588
  this.activeWorkflowRunRef = null;
481
589
  }
590
+ this.activeStepCapture = null;
482
591
  this.runner?.cancel();
483
592
  }
593
+
594
+ private startStepCapture(): ActiveStepCapture {
595
+ const capture: ActiveStepCapture = {
596
+ evidence: [],
597
+ pendingTools: new Map(),
598
+ rawText: "",
599
+ };
600
+ this.activeStepCapture = capture;
601
+ return capture;
602
+ }
603
+
604
+ private installWorkflowEventCapture(pi: ExtensionAPI): void {
605
+ const on = (pi as { on?: unknown }).on;
606
+ if (this.workflowEventCaptureInstalled || typeof on !== "function") return;
607
+ this.workflowEventCaptureInstalled = true;
608
+
609
+ pi.on("message_update", (event) => {
610
+ const capture = this.activeStepCapture;
611
+ if (!capture || event.assistantMessageEvent.type !== "text_delta") return;
612
+ capture.rawText += event.assistantMessageEvent.delta;
613
+ });
614
+
615
+ pi.on("tool_execution_start", (event) => {
616
+ const capture = this.activeStepCapture;
617
+ if (!capture) return;
618
+ capture.pendingTools.set(event.toolCallId, {
619
+ tool: event.toolName,
620
+ args: event.args ?? {},
621
+ startedAt: new Date().toISOString(),
622
+ });
623
+ });
624
+
625
+ pi.on("tool_execution_end", (event) => {
626
+ const capture = this.activeStepCapture;
627
+ if (!capture) return;
628
+ const completedAt = new Date().toISOString();
629
+ const pending = capture.pendingTools.get(event.toolCallId);
630
+ capture.pendingTools.delete(event.toolCallId);
631
+ const tool = pending?.tool ?? event.toolName;
632
+ if (!tool) return;
633
+ capture.evidence.push(
634
+ toolEvidenceRecord({
635
+ tool,
636
+ args: pending?.args ?? {},
637
+ result: event.result,
638
+ startedAt: pending?.startedAt ?? completedAt,
639
+ completedAt,
640
+ isError: event.isError === true,
641
+ }),
642
+ );
643
+ });
644
+ }
645
+
646
+ private prepareWorkflowPrompt(
647
+ pi: ExtensionAPI,
648
+ prompt: string,
649
+ stepType: string,
650
+ run: WorkflowRun | null,
651
+ ): string {
652
+ if (stepType !== "synthesis") return prompt;
653
+ const parsedAnalysts = collectParsedAnalystOutputs(run);
654
+ const tallyBlock = buildAnalystVoteTallyBlock(parsedAnalysts);
655
+ if (!tallyBlock) {
656
+ this.eventLogger?.log(run?.runId ?? "unknown", run?.currentStepIndex ?? 0, "tally_skipped", {
657
+ reason: "tally_skipped",
658
+ parsedAnalystCount: parsedAnalysts.length,
659
+ });
660
+ this.appendWorkflowEvent(pi, "tally_skipped", {
661
+ reason: "fewer_than_2_parsed_analysts",
662
+ parsedAnalystCount: parsedAnalysts.length,
663
+ });
664
+ return prompt;
665
+ }
666
+ this.eventLogger?.log(run?.runId ?? "unknown", run?.currentStepIndex ?? 0, "tally_injected", {
667
+ tallyBlock,
668
+ parsedAnalystCount: parsedAnalysts.length,
669
+ });
670
+ this.appendWorkflowEvent(pi, "tally_injected", {
671
+ tallyBlock,
672
+ parsedAnalystCount: parsedAnalysts.length,
673
+ });
674
+ return `${tallyBlock}\n\n${prompt}`;
675
+ }
676
+
677
+ private emitSynthesisValidation(
678
+ pi: ExtensionAPI,
679
+ runId: string,
680
+ stepIndex: number,
681
+ run: WorkflowRun | null,
682
+ currentOutput: StepOutput,
683
+ ): void {
684
+ const validationInput = collectValidationInput(run, currentOutput);
685
+ const mismatches = checkNumberMatch(
686
+ validationInput.evidence,
687
+ validationInput.toolResults,
688
+ ).filter((entry) => entry.type === "failure");
689
+ const passed = mismatches.length === 0;
690
+ const payload = {
691
+ passed,
692
+ mismatches,
693
+ skipped_unparsed: validationInput.skippedUnparsed,
694
+ };
695
+ this.eventLogger?.log(runId, stepIndex, passed ? "validation_passed" : "validation_failed", {
696
+ mismatches,
697
+ skipped_unparsed: validationInput.skippedUnparsed,
698
+ });
699
+ pi.appendEntry("opencandle-validation", payload);
700
+ this.appendWorkflowEvent(pi, passed ? "validation_passed" : "validation_failed", {
701
+ mismatches,
702
+ skipped_unparsed: validationInput.skippedUnparsed,
703
+ });
704
+ }
705
+
706
+ private appendWorkflowEvent(
707
+ pi: ExtensionAPI,
708
+ eventType: string,
709
+ payload: Record<string, unknown>,
710
+ ): void {
711
+ pi.appendEntry("opencandle-workflow-event", {
712
+ eventType,
713
+ ...payload,
714
+ });
715
+ }
716
+ }
717
+
718
+ function capturedText(capture: ActiveStepCapture, entries: SessionEntry[]): string {
719
+ return capture.rawText.trim() || extractAssistantText(entries);
720
+ }
721
+
722
+ function capturedEvidence(capture: ActiveStepCapture, entries: SessionEntry[]): EvidenceRecord[] {
723
+ return capture.evidence.length > 0 ? [...capture.evidence] : captureToolEvidence(entries);
724
+ }
725
+
726
+ function collectParsedAnalystOutputs(run: WorkflowRun | null): AnalystOutput[] {
727
+ if (!run) return [];
728
+ const outputs: AnalystOutput[] = [];
729
+ for (const output of run.stepOutputs.values()) {
730
+ if (output.parsed === true && output.analystOutput) {
731
+ outputs.push(output.analystOutput);
732
+ }
733
+ }
734
+ return outputs;
735
+ }
736
+
737
+ function shouldSkipRebuttal(run: WorkflowRun | null, stepType: string): boolean {
738
+ if (stepType !== "debate_rebuttal") return false;
739
+ const parsedAnalysts = collectParsedAnalystOutputs(run);
740
+ // Degradation rule: with fewer than two parsed analyst outputs there is no
741
+ // trustworthy consensus signal to gate on — run the rebuttal exactly as the
742
+ // status quo instead of skipping because isAnalystSplit([]) is false.
743
+ if (parsedAnalysts.length < 2) return false;
744
+ return !isAnalystSplit(parsedAnalysts);
745
+ }
746
+
747
+ function collectValidationInput(
748
+ run: WorkflowRun | null,
749
+ currentOutput?: StepOutput,
750
+ ): {
751
+ evidence: EvidenceRecord[];
752
+ toolResults: Map<string, number>;
753
+ skippedUnparsed: string[];
754
+ } {
755
+ const evidence: EvidenceRecord[] = [];
756
+ const toolResults = new Map<string, number>();
757
+ const skippedUnparsed: string[] = [];
758
+ const outputs = run ? [...run.stepOutputs.values()] : [];
759
+ if (currentOutput) {
760
+ outputs.push(currentOutput);
761
+ }
762
+
763
+ for (const output of outputs) {
764
+ if (output.stepType.startsWith("analyst_") && output.parsed === false) {
765
+ skippedUnparsed.push(output.stepType);
766
+ }
767
+ for (const record of output.evidence) {
768
+ collectToolNumbers(record, toolResults);
769
+ }
770
+ }
771
+
772
+ for (const output of outputs) {
773
+ if (!output.rawText) continue;
774
+ evidence.push(...extractNumericClaims(output.rawText, toolResults));
775
+ }
776
+
777
+ return { evidence, toolResults, skippedUnparsed };
778
+ }
779
+
780
+ function toolEvidenceRecord(input: {
781
+ tool: string;
782
+ args: unknown;
783
+ result: unknown;
784
+ startedAt: string;
785
+ completedAt: string;
786
+ isError: boolean;
787
+ }): EvidenceRecord {
788
+ const serializedResult = serialize(input.result);
789
+ const freshness = extractFreshness(input.result);
790
+ return {
791
+ label: `tool:${input.tool}`,
792
+ value: {
793
+ tool: input.tool,
794
+ args: truncate(serialize(input.args), 500),
795
+ ...(freshness ? { freshness } : {}),
796
+ resultDigest: {
797
+ preview: truncate(serializedResult, 500),
798
+ totalLength: serializedResult.length,
799
+ },
800
+ startedAt: input.startedAt,
801
+ completedAt: input.completedAt,
802
+ },
803
+ provenance: {
804
+ source: "computed",
805
+ timestamp: freshness?.providerDataAt ?? freshness?.fetchedAt ?? input.completedAt,
806
+ provider: input.tool,
807
+ confidence: input.isError ? 0.5 : undefined,
808
+ },
809
+ };
810
+ }
811
+
812
+ function summarizeStepEvidence(evidence: EvidenceRecord[]): unknown[] {
813
+ return evidence.map((record) => (isPlainObject(record.value) ? record.value : record));
814
+ }
815
+
816
+ function serialize(value: unknown): string {
817
+ if (typeof value === "string") return value;
818
+ try {
819
+ return JSON.stringify(value);
820
+ } catch {
821
+ return String(value);
822
+ }
823
+ }
824
+
825
+ function truncate(value: string, maxLength: number): string {
826
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
827
+ }
828
+
829
+ function extractFreshness(value: unknown): FreshnessStamp | undefined {
830
+ const record = isPlainObject(value) ? value : {};
831
+ const direct = record.freshness;
832
+ if (isFreshnessStamp(direct)) return direct;
833
+ const details = isPlainObject(record.details) ? record.details : {};
834
+ return isFreshnessStamp(details.freshness) ? details.freshness : undefined;
835
+ }
836
+
837
+ function isFreshnessStamp(value: unknown): value is FreshnessStamp {
838
+ const record = isPlainObject(value) ? value : {};
839
+ return (
840
+ typeof record.fetchedAt === "string" &&
841
+ typeof record.cacheStatus === "string" &&
842
+ typeof record.marketSession === "string" &&
843
+ typeof record.isStaleForSession === "boolean"
844
+ );
484
845
  }
485
846
 
486
847
  function formatToolDefaultsForPrompt(): string[] {
@@ -511,6 +872,89 @@ function flattenDefaults(defaults: Record<string, unknown>, prefix = ""): Array<
511
872
  return out;
512
873
  }
513
874
 
875
+ function isStructuredAnalystStep(stepType: string): boolean {
876
+ return stepType.startsWith("analyst_") || stepType.startsWith("debate_");
877
+ }
878
+
879
+ function hasStructuredContract(stepType: string, text: string): boolean {
880
+ if (stepType.startsWith("analyst_")) {
881
+ // The conviction range check must match extractConviction's 1-10
882
+ // contract: parseAnalystOutput silently defaults out-of-range values to
883
+ // 5, so accepting them here would record a fabricated conviction.
884
+ const conviction = text.match(/CONVICTION:\s*(\d+)/i);
885
+ const convictionInRange =
886
+ conviction !== null && Number(conviction[1]) >= 1 && Number(conviction[1]) <= 10;
887
+ return (
888
+ /SIGNAL:\s*(BUY|HOLD|SELL)/i.test(text) && convictionInRange && /THESIS:\s*(.+)/i.test(text)
889
+ );
890
+ }
891
+ if (stepType === "debate_bull") {
892
+ return (
893
+ /BULL THESIS:\s*(.+)/i.test(text) && /KEY RISK(?:\s+TO THIS THESIS)?:\s*(.+)/i.test(text)
894
+ );
895
+ }
896
+ if (stepType === "debate_bear") {
897
+ return /BEAR THESIS:\s*(.+)/i.test(text) && /WHAT WOULD CHANGE MY MIND:\s*(.+)/i.test(text);
898
+ }
899
+ if (stepType === "debate_rebuttal") {
900
+ return (
901
+ /^REBUTTAL SKIPPED/i.test(text.trim()) ||
902
+ (/CONCESSIONS:\s*[\s\S]+/i.test(text) && /REMAINING CONVICTION:\s*(\d+)/i.test(text))
903
+ );
904
+ }
905
+ return false;
906
+ }
907
+
908
+ function attachStructuredOutput(pi: ExtensionAPI, output: ReturnType<typeof promptStepOutput>) {
909
+ if (!isStructuredAnalystStep(output.stepType)) return output;
910
+
911
+ const rawText = output.rawText ?? "";
912
+ const evidence = summarizeStepEvidence(output.evidence);
913
+ const evidenceCount = output.evidence.length;
914
+ if (!hasStructuredContract(output.stepType, rawText)) {
915
+ const role = analystRoleFromStep(output.stepType);
916
+ pi.appendEntry("opencandle-analyst-step", {
917
+ stage: output.stepType,
918
+ ...(role ? { role } : {}),
919
+ parsed: false,
920
+ evidenceCount,
921
+ evidence,
922
+ });
923
+ return { ...output, parsed: false };
924
+ }
925
+
926
+ if (output.stepType.startsWith("analyst_")) {
927
+ const role = analystRoleFromStep(output.stepType) ?? "analyst";
928
+ const analystOutput = parseAnalystOutput(role, rawText);
929
+ const parsedOutput = { ...output, analystOutput, parsed: true };
930
+ pi.appendEntry("opencandle-analyst-step", {
931
+ stage: output.stepType,
932
+ role,
933
+ signal: analystOutput.signal,
934
+ conviction: analystOutput.conviction,
935
+ parsed: true,
936
+ evidenceCount,
937
+ evidence,
938
+ });
939
+ return parsedOutput;
940
+ }
941
+
942
+ const side = output.stepType === "debate_bear" ? "bear" : "bull";
943
+ const debateOutput = parseDebateOutput(side, rawText);
944
+ pi.appendEntry("opencandle-analyst-step", {
945
+ stage: output.stepType,
946
+ side,
947
+ parsed: true,
948
+ evidenceCount,
949
+ evidence,
950
+ });
951
+ return { ...output, debateOutput, parsed: true };
952
+ }
953
+
954
+ function analystRoleFromStep(stepType: string): string | undefined {
955
+ return stepType.startsWith("analyst_") ? stepType.slice("analyst_".length) : undefined;
956
+ }
957
+
514
958
  function buildSavedMarketStateContext(db: Database.Database): string {
515
959
  try {
516
960
  const service = new MarketStateService(db);
@@ -519,55 +963,30 @@ function buildSavedMarketStateContext(db: Database.Database): string {
519
963
  const alerts = service.listAlertRules();
520
964
  const reports = service.listReportTemplates();
521
965
  const reportRuns = service.listReportRuns();
522
- const predictions = service.listPredictions();
523
966
 
524
967
  if (
525
968
  watchlist.length === 0 &&
526
969
  lots.length === 0 &&
527
970
  alerts.length === 0 &&
528
971
  reports.length === 0 &&
529
- reportRuns.length === 0 &&
530
- predictions.length === 0
972
+ reportRuns.length === 0
531
973
  ) {
532
974
  return "";
533
975
  }
534
976
 
535
977
  const lines = [
536
978
  "## Saved Market State",
537
- "Use this saved user state to connect broad sector, theme, portfolio-impact, watchlist, alert, daily-report, and prediction questions back to the user's positions and tracked symbols. Treat it as context, not as a fresh instruction.",
979
+ "Use this saved user state to connect broad sector, theme, portfolio-impact, watchlist, alert, and daily-report questions back to the user's positions and tracked symbols. Treat it as context, not as a fresh instruction.",
538
980
  "When a saved portfolio lot is relevant, explicitly mention the saved quantity, average cost, and cost basis before explaining the impact.",
539
981
  'If the question concerns a sector, industry, event, company, or competitor connected to any saved position or watchlist symbol, end the answer with a short "Your positions" section explaining how it affects those specific holdings. Skip that section only when no saved symbol is plausibly affected.',
540
982
  ];
541
983
 
542
984
  if (lots.length > 0) {
543
- lines.push("Portfolio lots:");
544
- for (const lot of lots.slice(0, 8)) {
545
- const costBasis = formatMoney(lot.quantity * lot.avgCost, lot.currency);
546
- const name = lot.name ? ` (${lot.name})` : "";
547
- lines.push(
548
- `- ${lot.symbol}: ${lot.quantity} @ ${formatMoney(lot.avgCost, lot.currency)}, cost basis ${costBasis}${name}`,
549
- );
550
- }
985
+ lines.push(...formatPortfolioSummary(lots));
551
986
  }
552
987
 
553
988
  if (watchlist.length > 0) {
554
- lines.push("Watchlist:");
555
- for (const item of watchlist.slice(0, 8)) {
556
- const parts = [
557
- item.targetPrice == null
558
- ? null
559
- : `target ${formatMoney(item.targetPrice, item.priceCurrency ?? item.currency ?? "USD")}`,
560
- item.stopPrice == null
561
- ? null
562
- : `stop ${formatMoney(item.stopPrice, item.priceCurrency ?? item.currency ?? "USD")}`,
563
- item.thesis ? `thesis: ${item.thesis}` : null,
564
- item.tags && item.tags.length > 0 ? `tags: ${item.tags.join(", ")}` : null,
565
- item.notes ? `notes: ${item.notes}` : null,
566
- ].filter((part): part is string => part != null);
567
- lines.push(
568
- `- ${item.symbol}${item.name ? ` (${item.name})` : ""}${parts.length > 0 ? ` — ${parts.join("; ")}` : ""}`,
569
- );
570
- }
989
+ lines.push(...formatWatchlistSummary(watchlist));
571
990
  }
572
991
 
573
992
  if (alerts.length > 0) {
@@ -591,23 +1010,7 @@ function buildSavedMarketStateContext(db: Database.Database): string {
591
1010
  }
592
1011
 
593
1012
  if (reportRuns.length > 0) {
594
- const latest = reportRuns[0];
595
- lines.push(
596
- `Latest report run: ${latest.status} at ${latest.completedAt ?? latest.startedAt}`,
597
- );
598
- }
599
-
600
- if (predictions.length > 0) {
601
- lines.push("Predictions:");
602
- for (const prediction of predictions.slice(0, 8)) {
603
- const target =
604
- prediction.targetPrice == null
605
- ? ""
606
- : ` target ${formatMoney(prediction.targetPrice, "USD")}`;
607
- lines.push(
608
- `- #${prediction.id} ${prediction.symbol}: ${prediction.direction} conv ${prediction.conviction}/10 from ${formatMoney(prediction.entryPrice, "USD")}${target}, status ${prediction.status}, expires ${prediction.expiresAt}`,
609
- );
610
- }
1013
+ lines.push(...formatLatestReportSummary(reportRuns[0], { includeSummary: false }));
611
1014
  }
612
1015
 
613
1016
  return lines.join("\n");
@@ -629,19 +1032,6 @@ function shouldIncludeSavedMarketStateContext(
629
1032
  return workflowType != null || fallbackContext != null;
630
1033
  }
631
1034
 
632
- function formatMoney(value: number, currency: string): string {
633
- const normalized = currency.toUpperCase();
634
- if (normalized === "USD") return `$${value.toFixed(2)}`;
635
- return `${normalized} ${value.toFixed(2)}`;
636
- }
637
-
638
- function formatJsonSummary(value: unknown): string {
639
- if (value == null) return "";
640
- const json = JSON.stringify(value);
641
- if (json.length <= 90) return json;
642
- return `${json.slice(0, 87)}...`;
643
- }
644
-
645
1035
  function isPlainObject(value: unknown): value is Record<string, unknown> {
646
1036
  return typeof value === "object" && value !== null && !Array.isArray(value);
647
1037
  }
@@ -187,7 +187,7 @@ export class RuntimeValidator {
187
187
  export function checkNumberMatch(
188
188
  evidence: EvidenceRecord[],
189
189
  toolResults: Map<string, number>,
190
- ): ValidationEntry[] {
190
+ ): Array<ValidationEntry & { type: "pass" | "failure" }> {
191
191
  const results: (ValidationEntry & { type: "pass" | "failure" })[] = [];
192
192
  for (const record of evidence) {
193
193
  if (typeof record.value !== "number") continue;
@@ -12,6 +12,8 @@ export type WorkflowEventType =
12
12
  | "step_skipped"
13
13
  | "tool_called"
14
14
  | "tool_failed"
15
+ | "tally_injected"
16
+ | "tally_skipped"
15
17
  | "validation_passed"
16
18
  | "validation_failed"
17
19
  | "workflow_completed"
@@ -44,6 +44,9 @@ export interface StepOutput {
44
44
  stepType: string;
45
45
  evidence: EvidenceRecord[];
46
46
  rawText?: string;
47
+ analystOutput?: AnalystOutput;
48
+ debateOutput?: DebateOutput;
49
+ parsed?: boolean;
47
50
  }
48
51
 
49
52
  /** Analyst signal direction. */