opencandle 0.11.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 (162) hide show
  1. package/README.md +5 -4
  2. package/dist/analysts/orchestrator.d.ts +2 -0
  3. package/dist/analysts/orchestrator.js +19 -0
  4. package/dist/analysts/orchestrator.js.map +1 -1
  5. package/dist/doctor/report.js +1 -1
  6. package/dist/doctor/report.js.map +1 -1
  7. package/dist/infra/cache.d.ts +6 -0
  8. package/dist/infra/cache.js +16 -6
  9. package/dist/infra/cache.js.map +1 -1
  10. package/dist/infra/freshness.d.ts +21 -0
  11. package/dist/infra/freshness.js +119 -0
  12. package/dist/infra/freshness.js.map +1 -0
  13. package/dist/infra/market-calendar.d.ts +14 -0
  14. package/dist/infra/market-calendar.js +137 -0
  15. package/dist/infra/market-calendar.js.map +1 -0
  16. package/dist/infra/rate-limiter.js +1 -0
  17. package/dist/infra/rate-limiter.js.map +1 -1
  18. package/dist/market-state/summaries.d.ts +8 -0
  19. package/dist/market-state/summaries.js +70 -0
  20. package/dist/market-state/summaries.js.map +1 -0
  21. package/dist/onboarding/providers.d.ts +25 -1
  22. package/dist/onboarding/providers.js +26 -0
  23. package/dist/onboarding/providers.js.map +1 -1
  24. package/dist/pi/opencandle-extension.js +44 -5
  25. package/dist/pi/opencandle-extension.js.map +1 -1
  26. package/dist/providers/alpha-vantage.js +6 -0
  27. package/dist/providers/alpha-vantage.js.map +1 -1
  28. package/dist/providers/coingecko.js +1 -0
  29. package/dist/providers/coingecko.js.map +1 -1
  30. package/dist/providers/polymarket.d.ts +2 -0
  31. package/dist/providers/polymarket.js +163 -0
  32. package/dist/providers/polymarket.js.map +1 -0
  33. package/dist/providers/wrap-provider.js +5 -3
  34. package/dist/providers/wrap-provider.js.map +1 -1
  35. package/dist/providers/yahoo-finance.d.ts +3 -0
  36. package/dist/providers/yahoo-finance.js +206 -10
  37. package/dist/providers/yahoo-finance.js.map +1 -1
  38. package/dist/routing/entity-extractor.js +16 -0
  39. package/dist/routing/entity-extractor.js.map +1 -1
  40. package/dist/routing/route-manifest.js +1 -1
  41. package/dist/routing/route-manifest.js.map +1 -1
  42. package/dist/routing/router-llm-client.js +18 -3
  43. package/dist/routing/router-llm-client.js.map +1 -1
  44. package/dist/routing/router.d.ts +1 -1
  45. package/dist/routing/router.js +274 -5
  46. package/dist/routing/router.js.map +1 -1
  47. package/dist/routing/slot-resolver.d.ts +1 -0
  48. package/dist/routing/slot-resolver.js +1 -1
  49. package/dist/routing/slot-resolver.js.map +1 -1
  50. package/dist/runtime/evidence.d.ts +1 -0
  51. package/dist/runtime/evidence.js.map +1 -1
  52. package/dist/runtime/numeric-claims.d.ts +23 -0
  53. package/dist/runtime/numeric-claims.js +100 -0
  54. package/dist/runtime/numeric-claims.js.map +1 -0
  55. package/dist/runtime/planning-evidence.js +1 -76
  56. package/dist/runtime/planning-evidence.js.map +1 -1
  57. package/dist/runtime/prompt-step.d.ts +10 -1
  58. package/dist/runtime/prompt-step.js +116 -2
  59. package/dist/runtime/prompt-step.js.map +1 -1
  60. package/dist/runtime/session-coordinator.d.ts +11 -1
  61. package/dist/runtime/session-coordinator.js +352 -49
  62. package/dist/runtime/session-coordinator.js.map +1 -1
  63. package/dist/runtime/validation.d.ts +3 -1
  64. package/dist/runtime/workflow-events.d.ts +1 -1
  65. package/dist/runtime/workflow-events.js.map +1 -1
  66. package/dist/runtime/workflow-types.d.ts +3 -0
  67. package/dist/runtime/workflow-types.js.map +1 -1
  68. package/dist/tools/fundamentals/comps.d.ts +1 -0
  69. package/dist/tools/fundamentals/comps.js +20 -9
  70. package/dist/tools/fundamentals/comps.js.map +1 -1
  71. package/dist/tools/fundamentals/dcf.js +183 -112
  72. package/dist/tools/fundamentals/dcf.js.map +1 -1
  73. package/dist/tools/index.d.ts +1 -0
  74. package/dist/tools/index.js +3 -0
  75. package/dist/tools/index.js.map +1 -1
  76. package/dist/tools/macro/event-probabilities.d.ts +8 -0
  77. package/dist/tools/macro/event-probabilities.js +121 -0
  78. package/dist/tools/macro/event-probabilities.js.map +1 -0
  79. package/dist/tools/market/crypto-price.d.ts +4 -1
  80. package/dist/tools/market/crypto-price.js +10 -1
  81. package/dist/tools/market/crypto-price.js.map +1 -1
  82. package/dist/tools/market/screen-stocks.js +14 -3
  83. package/dist/tools/market/screen-stocks.js.map +1 -1
  84. package/dist/tools/market/stock-quote.d.ts +4 -1
  85. package/dist/tools/market/stock-quote.js +9 -4
  86. package/dist/tools/market/stock-quote.js.map +1 -1
  87. package/dist/tools/options/option-chain.d.ts +4 -1
  88. package/dist/tools/options/option-chain.js +12 -1
  89. package/dist/tools/options/option-chain.js.map +1 -1
  90. package/dist/tools/portfolio/risk-analysis.js +1 -1
  91. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  92. package/dist/tools/technical/indicators.js +1 -1
  93. package/dist/tools/technical/indicators.js.map +1 -1
  94. package/dist/types/fundamentals.d.ts +1 -0
  95. package/dist/types/index.d.ts +1 -0
  96. package/dist/types/market.d.ts +2 -0
  97. package/dist/types/options.d.ts +1 -0
  98. package/dist/types/prediction-markets.d.ts +13 -0
  99. package/dist/types/prediction-markets.js +2 -0
  100. package/dist/types/prediction-markets.js.map +1 -0
  101. package/gui/server/chat-event-adapter.ts +89 -5
  102. package/gui/server/http-routes.ts +285 -37
  103. package/gui/server/invoke-tool.ts +14 -13
  104. package/gui/server/live-chat-event-adapter.ts +37 -2
  105. package/gui/server/market-state-api.ts +63 -5
  106. package/gui/server/projector.ts +132 -3
  107. package/gui/server/session-actions.ts +32 -40
  108. package/gui/server/session-entry-wait.ts +22 -3
  109. package/gui/server/ws-hub.ts +16 -4
  110. package/gui/shared/chat-events.ts +10 -1
  111. package/gui/shared/event-reducer.ts +1 -0
  112. package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
  113. package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
  114. package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
  115. package/gui/web/dist/index.html +2 -2
  116. package/package.json +8 -13
  117. package/src/analysts/orchestrator.ts +20 -0
  118. package/src/doctor/report.ts +1 -1
  119. package/src/infra/cache.ts +24 -11
  120. package/src/infra/freshness.ts +165 -0
  121. package/src/infra/market-calendar.ts +193 -0
  122. package/src/infra/rate-limiter.ts +1 -0
  123. package/src/market-state/summaries.ts +75 -0
  124. package/src/onboarding/providers.ts +29 -1
  125. package/src/pi/opencandle-extension.ts +55 -5
  126. package/src/providers/alpha-vantage.ts +6 -0
  127. package/src/providers/coingecko.ts +2 -0
  128. package/src/providers/polymarket.ts +214 -0
  129. package/src/providers/wrap-provider.ts +5 -3
  130. package/src/providers/yahoo-finance.ts +339 -27
  131. package/src/routing/entity-extractor.ts +17 -0
  132. package/src/routing/route-manifest.ts +1 -1
  133. package/src/routing/router-llm-client.ts +31 -19
  134. package/src/routing/router.ts +340 -5
  135. package/src/routing/slot-resolver.ts +1 -1
  136. package/src/runtime/evidence.ts +1 -0
  137. package/src/runtime/numeric-claims.ts +108 -0
  138. package/src/runtime/planning-evidence.ts +6 -91
  139. package/src/runtime/prompt-step.ts +127 -2
  140. package/src/runtime/session-coordinator.ts +462 -57
  141. package/src/runtime/validation.ts +1 -1
  142. package/src/runtime/workflow-events.ts +2 -0
  143. package/src/runtime/workflow-types.ts +3 -0
  144. package/src/tools/fundamentals/comps.ts +25 -14
  145. package/src/tools/fundamentals/dcf.ts +204 -124
  146. package/src/tools/index.ts +3 -0
  147. package/src/tools/macro/event-probabilities.ts +141 -0
  148. package/src/tools/market/crypto-price.ts +14 -2
  149. package/src/tools/market/screen-stocks.ts +14 -3
  150. package/src/tools/market/stock-quote.ts +49 -43
  151. package/src/tools/options/option-chain.ts +16 -2
  152. package/src/tools/portfolio/risk-analysis.ts +1 -1
  153. package/src/tools/technical/indicators.ts +1 -1
  154. package/src/types/fundamentals.ts +1 -0
  155. package/src/types/index.ts +1 -0
  156. package/src/types/market.ts +2 -0
  157. package/src/types/options.ts +1 -0
  158. package/src/types/prediction-markets.ts +13 -0
  159. package/gui/server/chat-run-session.ts +0 -16
  160. package/gui/web/dist/assets/CatalogOverlay-ChRTKNlf.js +0 -1
  161. package/gui/web/dist/assets/index-BzyqyVnd.css +0 -2
  162. package/gui/web/dist/assets/index-DvMjkOP9.js +0 -65
@@ -15,13 +15,95 @@ export function promptStep(stepType, description, prompt, options = {}) {
15
15
  * Create a StepOutput for a prompt-based step (no structured evidence yet).
16
16
  * Evidence will be captured separately via tool call hooks.
17
17
  */
18
- export function promptStepOutput(stepIndex, stepType) {
18
+ export function promptStepOutput(stepIndex, stepType, options = {}) {
19
19
  return {
20
20
  stepIndex,
21
21
  stepType,
22
- evidence: [],
22
+ evidence: options.evidence ?? [],
23
+ ...(options.rawText ? { rawText: options.rawText } : {}),
23
24
  };
24
25
  }
26
+ /** Capture compact evidence records from session entries produced by one step. */
27
+ export function captureToolEvidence(entries) {
28
+ const calls = new Map();
29
+ const evidence = [];
30
+ for (const entry of entries) {
31
+ if (entry.type !== "message")
32
+ continue;
33
+ const message = entry.message;
34
+ if (message.role === "assistant") {
35
+ for (const part of Array.isArray(message.content) ? message.content : []) {
36
+ const record = asRecord(part);
37
+ if (record.type !== "toolCall")
38
+ continue;
39
+ const id = stringValue(record.id);
40
+ const tool = stringValue(record.name);
41
+ if (!id || !tool)
42
+ continue;
43
+ calls.set(id, {
44
+ tool,
45
+ args: record.arguments ?? record.args ?? {},
46
+ startedAt: entry.timestamp,
47
+ });
48
+ }
49
+ continue;
50
+ }
51
+ if (message.role !== "toolResult")
52
+ continue;
53
+ const toolCallId = stringValue(message.toolCallId);
54
+ const pending = toolCallId ? calls.get(toolCallId) : undefined;
55
+ const tool = pending?.tool ?? stringValue(message.toolName);
56
+ if (!tool)
57
+ continue;
58
+ const resultValue = message.details ?? message.content ?? "";
59
+ const freshness = extractFreshness(resultValue);
60
+ const serializedResult = serialize(resultValue);
61
+ evidence.push({
62
+ label: `tool:${tool}`,
63
+ value: {
64
+ tool,
65
+ args: truncate(serialize(pending?.args ?? {}), 500),
66
+ ...(freshness ? { freshness } : {}),
67
+ resultDigest: {
68
+ preview: truncate(serializedResult, 500),
69
+ totalLength: serializedResult.length,
70
+ },
71
+ startedAt: pending?.startedAt ?? entry.timestamp,
72
+ completedAt: entry.timestamp,
73
+ },
74
+ provenance: {
75
+ source: "computed",
76
+ timestamp: freshness?.providerDataAt ?? freshness?.fetchedAt ?? entry.timestamp,
77
+ provider: tool,
78
+ confidence: message.isError === true ? 0.5 : undefined,
79
+ },
80
+ });
81
+ }
82
+ return evidence;
83
+ }
84
+ /** Extract assistant text emitted during a step, preserving final response order. */
85
+ export function extractAssistantText(entries) {
86
+ return entries
87
+ .filter((entry) => entry.type === "message")
88
+ .flatMap((entry) => {
89
+ const message = entry
90
+ .message;
91
+ if (message.role !== "assistant")
92
+ return [];
93
+ const content = message.content;
94
+ if (typeof content === "string")
95
+ return [content];
96
+ if (!Array.isArray(content))
97
+ return [];
98
+ return content
99
+ .map((part) => asRecord(part))
100
+ .filter((part) => part.type === "text")
101
+ .map((part) => stringValue(part.text) ?? "")
102
+ .filter(Boolean);
103
+ })
104
+ .join("\n")
105
+ .trim();
106
+ }
25
107
  /**
26
108
  * Extract just the WorkflowStep metadata from PromptStep definitions
27
109
  * (dropping the prompt field) for passing to WorkflowRunner.
@@ -29,4 +111,36 @@ export function promptStepOutput(stepIndex, stepType) {
29
111
  export function toStepDefinitions(steps) {
30
112
  return steps.map(({ prompt: _prompt, ...step }) => step);
31
113
  }
114
+ function serialize(value) {
115
+ if (typeof value === "string")
116
+ return value;
117
+ try {
118
+ return JSON.stringify(value);
119
+ }
120
+ catch {
121
+ return String(value);
122
+ }
123
+ }
124
+ function truncate(value, maxLength) {
125
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
126
+ }
127
+ function asRecord(value) {
128
+ return typeof value === "object" && value !== null && !Array.isArray(value)
129
+ ? value
130
+ : {};
131
+ }
132
+ function stringValue(value) {
133
+ return typeof value === "string" && value.length > 0 ? value : undefined;
134
+ }
135
+ function extractFreshness(value) {
136
+ const record = asRecord(value);
137
+ return isFreshnessStamp(record.freshness) ? record.freshness : undefined;
138
+ }
139
+ function isFreshnessStamp(value) {
140
+ const record = asRecord(value);
141
+ return (typeof record.fetchedAt === "string" &&
142
+ typeof record.cacheStatus === "string" &&
143
+ typeof record.marketSession === "string" &&
144
+ typeof record.isStaleForSession === "boolean");
145
+ }
32
146
  //# sourceMappingURL=prompt-step.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-step.js","sourceRoot":"","sources":["../../src/runtime/prompt-step.ts"],"names":[],"mappings":"AAkBA;;GAEG;AACH,MAAM,UAAU,UAAU,CACxB,QAAgB,EAChB,WAAmB,EACnB,MAAc,EACd,UAII,EAAE;IAEN,OAAO;QACL,QAAQ;QACR,WAAW;QACX,MAAM;QACN,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,KAAK;QACrC,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,EAAE;QAC5C,eAAe,EAAE,OAAO,CAAC,eAAe,IAAI,EAAE;KAC/C,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,QAAgB;IAClE,OAAO;QACL,SAAS;QACT,QAAQ;QACR,QAAQ,EAAE,EAAE;KACb,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAmB;IACnD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AAC3D,CAAC"}
1
+ {"version":3,"file":"prompt-step.js","sourceRoot":"","sources":["../../src/runtime/prompt-step.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
2
2
  import { MemoryStorage } from "../memory/index.js";
3
3
  /**
4
4
  * Alias for the session-manager handle extensions receive via
@@ -21,6 +21,9 @@ type QueueContext = ExtensionCommandContext | {
21
21
  ui?: {
22
22
  notify(message: string, level?: string): void;
23
23
  };
24
+ sessionManager?: {
25
+ getEntries(): SessionEntry[];
26
+ };
24
27
  };
25
28
  /**
26
29
  * Coordinates session lifecycle, memory, workflow execution,
@@ -35,6 +38,8 @@ export declare class SessionCoordinator {
35
38
  private providerTracker;
36
39
  private activeWorkflowRunRef;
37
40
  private activeWorkflowType;
41
+ private activeStepCapture;
42
+ private workflowEventCaptureInstalled;
38
43
  private tickerValidationCache;
39
44
  private sessionId;
40
45
  constructor();
@@ -129,5 +134,10 @@ export declare class SessionCoordinator {
129
134
  private startWorkflowRun;
130
135
  /** Cancel any active workflow. */
131
136
  cancelActiveWorkflow(): void;
137
+ private startStepCapture;
138
+ private installWorkflowEventCapture;
139
+ private prepareWorkflowPrompt;
140
+ private emitSynthesisValidation;
141
+ private appendWorkflowEvent;
132
142
  }
133
143
  export {};
@@ -1,13 +1,18 @@
1
- import { getAllDefaults, initDefaultDatabase, MemoryStorage } from "../memory/index.js";
1
+ import { isAnalystSplit, parseAnalystOutput, parseDebateOutput } from "../analysts/contracts.js";
2
+ import { buildAnalystVoteTallyBlock } from "../analysts/orchestrator.js";
2
3
  import { MarketStateService } from "../market-state/service.js";
4
+ import { formatJsonSummary, formatLatestReportSummary, formatPortfolioSummary, formatWatchlistSummary, } from "../market-state/summaries.js";
5
+ import { getAllDefaults, initDefaultDatabase, MemoryStorage } from "../memory/index.js";
3
6
  import { MemoryManager } from "../memory/manager.js";
4
7
  import { extractPreferences } from "../memory/preference-extractor.js";
5
8
  import { runOpenCandleSetup } from "../pi/setup.js";
6
9
  import { PromptContextBuilder } from "../prompts/context-builder.js";
7
10
  import { getAddonToolDescriptions } from "../tool-kit.js";
8
- import { promptStepOutput, toStepDefinitions } from "./prompt-step.js";
11
+ import { collectToolNumbers, extractNumericClaims } from "./numeric-claims.js";
12
+ import { captureToolEvidence, extractAssistantText, promptStepOutput, toStepDefinitions, } from "./prompt-step.js";
9
13
  import { ProviderTracker } from "./provider-tracker.js";
10
14
  import { clearRunContext, setRunContext } from "./run-context.js";
15
+ import { checkNumberMatch } from "./validation.js";
11
16
  import { WorkflowEventLogger } from "./workflow-events.js";
12
17
  import { WorkflowRunner } from "./workflow-runner.js";
13
18
  const PROMPT_SETTLE_POLL_MS = 25;
@@ -31,6 +36,10 @@ function hasPendingMessages(ctx) {
31
36
  function isReadyForNextPrompt(ctx) {
32
37
  return ctx.isIdle() && !hasPendingMessages(ctx);
33
38
  }
39
+ function readSessionEntries(ctx) {
40
+ const manager = "sessionManager" in ctx ? ctx.sessionManager : undefined;
41
+ return manager?.getEntries?.() ?? [];
42
+ }
34
43
  function sleep(ms) {
35
44
  return new Promise((resolve) => setTimeout(resolve, ms));
36
45
  }
@@ -68,6 +77,8 @@ export class SessionCoordinator {
68
77
  providerTracker;
69
78
  activeWorkflowRunRef = null;
70
79
  activeWorkflowType;
80
+ activeStepCapture = null;
81
+ workflowEventCaptureInstalled = false;
71
82
  tickerValidationCache = new Map();
72
83
  sessionId = "unknown";
73
84
  constructor() {
@@ -297,6 +308,7 @@ export class SessionCoordinator {
297
308
  startWorkflowRun(pi, definition, ctx, firstPromptMode) {
298
309
  if (definition.steps.length === 0)
299
310
  return;
311
+ this.installWorkflowEventCapture(pi);
300
312
  const runner = this.runner;
301
313
  if (this.activeWorkflowRunRef) {
302
314
  this.activeWorkflowRunRef.active = false;
@@ -321,27 +333,74 @@ export class SessionCoordinator {
321
333
  // Start the runner in the background for state tracking
322
334
  const stepDefs = toStepDefinitions(definition.steps);
323
335
  void runner
324
- .start(definition.workflowType, stepDefs, async (step, stepIndex) => {
325
- // First step was already sent above — just wait for settlement
336
+ .start(definition.workflowType, stepDefs, async (step, stepIndex, _priorEvidence, context) => {
337
+ let entriesBeforeStep = readSessionEntries(ctx).length;
338
+ const eventCapture = this.startStepCapture();
339
+ // First step was already sent above — later steps are sent here.
326
340
  if (stepIndex > 0) {
327
341
  const settled = await waitForPromptSettlement(ctx, () => runRef.active);
328
342
  if (!settled || !runRef.active) {
329
343
  throw new Error("run_cancelled");
330
344
  }
331
- pi.sendUserMessage(definition.steps[stepIndex].prompt);
345
+ if (shouldSkipRebuttal(runner.getActiveRun(), step.stepType)) {
346
+ this.appendWorkflowEvent(pi, "step_skipped", {
347
+ stepType: step.stepType,
348
+ reason: "analyst_consensus",
349
+ });
350
+ throw new Error("analyst_consensus");
351
+ }
352
+ entriesBeforeStep = readSessionEntries(ctx).length;
353
+ const prompt = this.prepareWorkflowPrompt(pi, definition.steps[stepIndex].prompt, step.stepType, runner.getActiveRun());
354
+ pi.sendUserMessage(prompt);
332
355
  }
333
- else {
334
- // For the first step, just wait for it to settle
335
- const settled = await waitForPromptSettlement(ctx, () => runRef.active, {
336
- requireActivity: firstPromptMode === "transform",
337
- });
338
- if (!settled || !runRef.active) {
356
+ const settled = await waitForPromptSettlement(ctx, () => runRef.active, {
357
+ requireActivity: stepIndex === 0 && firstPromptMode === "transform",
358
+ });
359
+ if (!settled || !runRef.active) {
360
+ throw new Error("run_cancelled");
361
+ }
362
+ let stepEntries = readSessionEntries(ctx).slice(entriesBeforeStep);
363
+ let rawText = capturedText(eventCapture, stepEntries);
364
+ let output = promptStepOutput(stepIndex, step.stepType, {
365
+ evidence: capturedEvidence(eventCapture, stepEntries),
366
+ rawText,
367
+ });
368
+ if (isStructuredAnalystStep(step.stepType) &&
369
+ !hasStructuredContract(step.stepType, rawText)) {
370
+ pi.sendUserMessage("Please revise your previous response to include the exact required final output format from the stage prompt. Do not add new tool calls.");
371
+ const retrySettled = await waitForPromptSettlement(ctx, () => runRef.active);
372
+ if (!retrySettled || !runRef.active) {
339
373
  throw new Error("run_cancelled");
340
374
  }
375
+ stepEntries = readSessionEntries(ctx).slice(entriesBeforeStep);
376
+ rawText = capturedText(eventCapture, stepEntries);
377
+ output = promptStepOutput(stepIndex, step.stepType, {
378
+ evidence: capturedEvidence(eventCapture, stepEntries),
379
+ rawText,
380
+ });
381
+ }
382
+ for (const record of output.evidence) {
383
+ const value = isPlainObject(record.value) ? record.value : {};
384
+ this.eventLogger?.log(context.runId, stepIndex, "tool_called", {
385
+ stepType: step.stepType,
386
+ tool: value.tool,
387
+ args: value.args,
388
+ resultDigest: value.resultDigest,
389
+ });
390
+ }
391
+ if (this.activeStepCapture === eventCapture) {
392
+ this.activeStepCapture = null;
393
+ }
394
+ const structuredOutput = attachStructuredOutput(pi, output);
395
+ if (step.stepType === "synthesis") {
396
+ this.emitSynthesisValidation(pi, context.runId, stepIndex, runner.getActiveRun(), structuredOutput);
341
397
  }
342
- return promptStepOutput(stepIndex, step.stepType);
398
+ return structuredOutput;
343
399
  })
344
400
  .finally(() => {
401
+ if (this.activeWorkflowRunRef === runRef) {
402
+ this.activeStepCapture = null;
403
+ }
345
404
  clearRunContext(runRef.contextToken);
346
405
  if (this.activeWorkflowRunRef === runRef) {
347
406
  this.activeWorkflowRunRef = null;
@@ -356,8 +415,216 @@ export class SessionCoordinator {
356
415
  clearRunContext(activeRef.contextToken);
357
416
  this.activeWorkflowRunRef = null;
358
417
  }
418
+ this.activeStepCapture = null;
359
419
  this.runner?.cancel();
360
420
  }
421
+ startStepCapture() {
422
+ const capture = {
423
+ evidence: [],
424
+ pendingTools: new Map(),
425
+ rawText: "",
426
+ };
427
+ this.activeStepCapture = capture;
428
+ return capture;
429
+ }
430
+ installWorkflowEventCapture(pi) {
431
+ const on = pi.on;
432
+ if (this.workflowEventCaptureInstalled || typeof on !== "function")
433
+ return;
434
+ this.workflowEventCaptureInstalled = true;
435
+ pi.on("message_update", (event) => {
436
+ const capture = this.activeStepCapture;
437
+ if (!capture || event.assistantMessageEvent.type !== "text_delta")
438
+ return;
439
+ capture.rawText += event.assistantMessageEvent.delta;
440
+ });
441
+ pi.on("tool_execution_start", (event) => {
442
+ const capture = this.activeStepCapture;
443
+ if (!capture)
444
+ return;
445
+ capture.pendingTools.set(event.toolCallId, {
446
+ tool: event.toolName,
447
+ args: event.args ?? {},
448
+ startedAt: new Date().toISOString(),
449
+ });
450
+ });
451
+ pi.on("tool_execution_end", (event) => {
452
+ const capture = this.activeStepCapture;
453
+ if (!capture)
454
+ return;
455
+ const completedAt = new Date().toISOString();
456
+ const pending = capture.pendingTools.get(event.toolCallId);
457
+ capture.pendingTools.delete(event.toolCallId);
458
+ const tool = pending?.tool ?? event.toolName;
459
+ if (!tool)
460
+ return;
461
+ capture.evidence.push(toolEvidenceRecord({
462
+ tool,
463
+ args: pending?.args ?? {},
464
+ result: event.result,
465
+ startedAt: pending?.startedAt ?? completedAt,
466
+ completedAt,
467
+ isError: event.isError === true,
468
+ }));
469
+ });
470
+ }
471
+ prepareWorkflowPrompt(pi, prompt, stepType, run) {
472
+ if (stepType !== "synthesis")
473
+ return prompt;
474
+ const parsedAnalysts = collectParsedAnalystOutputs(run);
475
+ const tallyBlock = buildAnalystVoteTallyBlock(parsedAnalysts);
476
+ if (!tallyBlock) {
477
+ this.eventLogger?.log(run?.runId ?? "unknown", run?.currentStepIndex ?? 0, "tally_skipped", {
478
+ reason: "tally_skipped",
479
+ parsedAnalystCount: parsedAnalysts.length,
480
+ });
481
+ this.appendWorkflowEvent(pi, "tally_skipped", {
482
+ reason: "fewer_than_2_parsed_analysts",
483
+ parsedAnalystCount: parsedAnalysts.length,
484
+ });
485
+ return prompt;
486
+ }
487
+ this.eventLogger?.log(run?.runId ?? "unknown", run?.currentStepIndex ?? 0, "tally_injected", {
488
+ tallyBlock,
489
+ parsedAnalystCount: parsedAnalysts.length,
490
+ });
491
+ this.appendWorkflowEvent(pi, "tally_injected", {
492
+ tallyBlock,
493
+ parsedAnalystCount: parsedAnalysts.length,
494
+ });
495
+ return `${tallyBlock}\n\n${prompt}`;
496
+ }
497
+ emitSynthesisValidation(pi, runId, stepIndex, run, currentOutput) {
498
+ const validationInput = collectValidationInput(run, currentOutput);
499
+ const mismatches = checkNumberMatch(validationInput.evidence, validationInput.toolResults).filter((entry) => entry.type === "failure");
500
+ const passed = mismatches.length === 0;
501
+ const payload = {
502
+ passed,
503
+ mismatches,
504
+ skipped_unparsed: validationInput.skippedUnparsed,
505
+ };
506
+ this.eventLogger?.log(runId, stepIndex, passed ? "validation_passed" : "validation_failed", {
507
+ mismatches,
508
+ skipped_unparsed: validationInput.skippedUnparsed,
509
+ });
510
+ pi.appendEntry("opencandle-validation", payload);
511
+ this.appendWorkflowEvent(pi, passed ? "validation_passed" : "validation_failed", {
512
+ mismatches,
513
+ skipped_unparsed: validationInput.skippedUnparsed,
514
+ });
515
+ }
516
+ appendWorkflowEvent(pi, eventType, payload) {
517
+ pi.appendEntry("opencandle-workflow-event", {
518
+ eventType,
519
+ ...payload,
520
+ });
521
+ }
522
+ }
523
+ function capturedText(capture, entries) {
524
+ return capture.rawText.trim() || extractAssistantText(entries);
525
+ }
526
+ function capturedEvidence(capture, entries) {
527
+ return capture.evidence.length > 0 ? [...capture.evidence] : captureToolEvidence(entries);
528
+ }
529
+ function collectParsedAnalystOutputs(run) {
530
+ if (!run)
531
+ return [];
532
+ const outputs = [];
533
+ for (const output of run.stepOutputs.values()) {
534
+ if (output.parsed === true && output.analystOutput) {
535
+ outputs.push(output.analystOutput);
536
+ }
537
+ }
538
+ return outputs;
539
+ }
540
+ function shouldSkipRebuttal(run, stepType) {
541
+ if (stepType !== "debate_rebuttal")
542
+ return false;
543
+ const parsedAnalysts = collectParsedAnalystOutputs(run);
544
+ // Degradation rule: with fewer than two parsed analyst outputs there is no
545
+ // trustworthy consensus signal to gate on — run the rebuttal exactly as the
546
+ // status quo instead of skipping because isAnalystSplit([]) is false.
547
+ if (parsedAnalysts.length < 2)
548
+ return false;
549
+ return !isAnalystSplit(parsedAnalysts);
550
+ }
551
+ function collectValidationInput(run, currentOutput) {
552
+ const evidence = [];
553
+ const toolResults = new Map();
554
+ const skippedUnparsed = [];
555
+ const outputs = run ? [...run.stepOutputs.values()] : [];
556
+ if (currentOutput) {
557
+ outputs.push(currentOutput);
558
+ }
559
+ for (const output of outputs) {
560
+ if (output.stepType.startsWith("analyst_") && output.parsed === false) {
561
+ skippedUnparsed.push(output.stepType);
562
+ }
563
+ for (const record of output.evidence) {
564
+ collectToolNumbers(record, toolResults);
565
+ }
566
+ }
567
+ for (const output of outputs) {
568
+ if (!output.rawText)
569
+ continue;
570
+ evidence.push(...extractNumericClaims(output.rawText, toolResults));
571
+ }
572
+ return { evidence, toolResults, skippedUnparsed };
573
+ }
574
+ function toolEvidenceRecord(input) {
575
+ const serializedResult = serialize(input.result);
576
+ const freshness = extractFreshness(input.result);
577
+ return {
578
+ label: `tool:${input.tool}`,
579
+ value: {
580
+ tool: input.tool,
581
+ args: truncate(serialize(input.args), 500),
582
+ ...(freshness ? { freshness } : {}),
583
+ resultDigest: {
584
+ preview: truncate(serializedResult, 500),
585
+ totalLength: serializedResult.length,
586
+ },
587
+ startedAt: input.startedAt,
588
+ completedAt: input.completedAt,
589
+ },
590
+ provenance: {
591
+ source: "computed",
592
+ timestamp: freshness?.providerDataAt ?? freshness?.fetchedAt ?? input.completedAt,
593
+ provider: input.tool,
594
+ confidence: input.isError ? 0.5 : undefined,
595
+ },
596
+ };
597
+ }
598
+ function summarizeStepEvidence(evidence) {
599
+ return evidence.map((record) => (isPlainObject(record.value) ? record.value : record));
600
+ }
601
+ function serialize(value) {
602
+ if (typeof value === "string")
603
+ return value;
604
+ try {
605
+ return JSON.stringify(value);
606
+ }
607
+ catch {
608
+ return String(value);
609
+ }
610
+ }
611
+ function truncate(value, maxLength) {
612
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
613
+ }
614
+ function extractFreshness(value) {
615
+ const record = isPlainObject(value) ? value : {};
616
+ const direct = record.freshness;
617
+ if (isFreshnessStamp(direct))
618
+ return direct;
619
+ const details = isPlainObject(record.details) ? record.details : {};
620
+ return isFreshnessStamp(details.freshness) ? details.freshness : undefined;
621
+ }
622
+ function isFreshnessStamp(value) {
623
+ const record = isPlainObject(value) ? value : {};
624
+ return (typeof record.fetchedAt === "string" &&
625
+ typeof record.cacheStatus === "string" &&
626
+ typeof record.marketSession === "string" &&
627
+ typeof record.isStaleForSession === "boolean");
361
628
  }
362
629
  function formatToolDefaultsForPrompt() {
363
630
  try {
@@ -387,6 +654,76 @@ function flattenDefaults(defaults, prefix = "") {
387
654
  }
388
655
  return out;
389
656
  }
657
+ function isStructuredAnalystStep(stepType) {
658
+ return stepType.startsWith("analyst_") || stepType.startsWith("debate_");
659
+ }
660
+ function hasStructuredContract(stepType, text) {
661
+ if (stepType.startsWith("analyst_")) {
662
+ // The conviction range check must match extractConviction's 1-10
663
+ // contract: parseAnalystOutput silently defaults out-of-range values to
664
+ // 5, so accepting them here would record a fabricated conviction.
665
+ const conviction = text.match(/CONVICTION:\s*(\d+)/i);
666
+ const convictionInRange = conviction !== null && Number(conviction[1]) >= 1 && Number(conviction[1]) <= 10;
667
+ return (/SIGNAL:\s*(BUY|HOLD|SELL)/i.test(text) && convictionInRange && /THESIS:\s*(.+)/i.test(text));
668
+ }
669
+ if (stepType === "debate_bull") {
670
+ return (/BULL THESIS:\s*(.+)/i.test(text) && /KEY RISK(?:\s+TO THIS THESIS)?:\s*(.+)/i.test(text));
671
+ }
672
+ if (stepType === "debate_bear") {
673
+ return /BEAR THESIS:\s*(.+)/i.test(text) && /WHAT WOULD CHANGE MY MIND:\s*(.+)/i.test(text);
674
+ }
675
+ if (stepType === "debate_rebuttal") {
676
+ return (/^REBUTTAL SKIPPED/i.test(text.trim()) ||
677
+ (/CONCESSIONS:\s*[\s\S]+/i.test(text) && /REMAINING CONVICTION:\s*(\d+)/i.test(text)));
678
+ }
679
+ return false;
680
+ }
681
+ function attachStructuredOutput(pi, output) {
682
+ if (!isStructuredAnalystStep(output.stepType))
683
+ return output;
684
+ const rawText = output.rawText ?? "";
685
+ const evidence = summarizeStepEvidence(output.evidence);
686
+ const evidenceCount = output.evidence.length;
687
+ if (!hasStructuredContract(output.stepType, rawText)) {
688
+ const role = analystRoleFromStep(output.stepType);
689
+ pi.appendEntry("opencandle-analyst-step", {
690
+ stage: output.stepType,
691
+ ...(role ? { role } : {}),
692
+ parsed: false,
693
+ evidenceCount,
694
+ evidence,
695
+ });
696
+ return { ...output, parsed: false };
697
+ }
698
+ if (output.stepType.startsWith("analyst_")) {
699
+ const role = analystRoleFromStep(output.stepType) ?? "analyst";
700
+ const analystOutput = parseAnalystOutput(role, rawText);
701
+ const parsedOutput = { ...output, analystOutput, parsed: true };
702
+ pi.appendEntry("opencandle-analyst-step", {
703
+ stage: output.stepType,
704
+ role,
705
+ signal: analystOutput.signal,
706
+ conviction: analystOutput.conviction,
707
+ parsed: true,
708
+ evidenceCount,
709
+ evidence,
710
+ });
711
+ return parsedOutput;
712
+ }
713
+ const side = output.stepType === "debate_bear" ? "bear" : "bull";
714
+ const debateOutput = parseDebateOutput(side, rawText);
715
+ pi.appendEntry("opencandle-analyst-step", {
716
+ stage: output.stepType,
717
+ side,
718
+ parsed: true,
719
+ evidenceCount,
720
+ evidence,
721
+ });
722
+ return { ...output, debateOutput, parsed: true };
723
+ }
724
+ function analystRoleFromStep(stepType) {
725
+ return stepType.startsWith("analyst_") ? stepType.slice("analyst_".length) : undefined;
726
+ }
390
727
  function buildSavedMarketStateContext(db) {
391
728
  try {
392
729
  const service = new MarketStateService(db);
@@ -409,29 +746,10 @@ function buildSavedMarketStateContext(db) {
409
746
  '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.',
410
747
  ];
411
748
  if (lots.length > 0) {
412
- lines.push("Portfolio lots:");
413
- for (const lot of lots.slice(0, 8)) {
414
- const costBasis = formatMoney(lot.quantity * lot.avgCost, lot.currency);
415
- const name = lot.name ? ` (${lot.name})` : "";
416
- lines.push(`- ${lot.symbol}: ${lot.quantity} @ ${formatMoney(lot.avgCost, lot.currency)}, cost basis ${costBasis}${name}`);
417
- }
749
+ lines.push(...formatPortfolioSummary(lots));
418
750
  }
419
751
  if (watchlist.length > 0) {
420
- lines.push("Watchlist:");
421
- for (const item of watchlist.slice(0, 8)) {
422
- const parts = [
423
- item.targetPrice == null
424
- ? null
425
- : `target ${formatMoney(item.targetPrice, item.priceCurrency ?? item.currency ?? "USD")}`,
426
- item.stopPrice == null
427
- ? null
428
- : `stop ${formatMoney(item.stopPrice, item.priceCurrency ?? item.currency ?? "USD")}`,
429
- item.thesis ? `thesis: ${item.thesis}` : null,
430
- item.tags && item.tags.length > 0 ? `tags: ${item.tags.join(", ")}` : null,
431
- item.notes ? `notes: ${item.notes}` : null,
432
- ].filter((part) => part != null);
433
- lines.push(`- ${item.symbol}${item.name ? ` (${item.name})` : ""}${parts.length > 0 ? ` — ${parts.join("; ")}` : ""}`);
434
- }
752
+ lines.push(...formatWatchlistSummary(watchlist));
435
753
  }
436
754
  if (alerts.length > 0) {
437
755
  lines.push("Alert rules:");
@@ -447,8 +765,7 @@ function buildSavedMarketStateContext(db) {
447
765
  }
448
766
  }
449
767
  if (reportRuns.length > 0) {
450
- const latest = reportRuns[0];
451
- lines.push(`Latest report run: ${latest.status} at ${latest.completedAt ?? latest.startedAt}`);
768
+ lines.push(...formatLatestReportSummary(reportRuns[0], { includeSummary: false }));
452
769
  }
453
770
  return lines.join("\n");
454
771
  }
@@ -464,20 +781,6 @@ function shouldIncludeSavedMarketStateContext(workflowType, resolvedTurnContext,
464
781
  // general finance or LLM-router fallback), never for pass-through prompts.
465
782
  return workflowType != null || fallbackContext != null;
466
783
  }
467
- function formatMoney(value, currency) {
468
- const normalized = currency.toUpperCase();
469
- if (normalized === "USD")
470
- return `$${value.toFixed(2)}`;
471
- return `${normalized} ${value.toFixed(2)}`;
472
- }
473
- function formatJsonSummary(value) {
474
- if (value == null)
475
- return "";
476
- const json = JSON.stringify(value);
477
- if (json.length <= 90)
478
- return json;
479
- return `${json.slice(0, 87)}...`;
480
- }
481
784
  function isPlainObject(value) {
482
785
  return typeof value === "object" && value !== null && !Array.isArray(value);
483
786
  }