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
@@ -1,3 +1,9 @@
1
+ import {
2
+ classifyMarketStatus,
3
+ KNOWN_US_MARKET_HOLIDAYS,
4
+ lastTradingDay,
5
+ localDateTimeParts,
6
+ } from "../infra/market-calendar.js";
1
7
  import type { ParsedTag } from "../onboarding/tool-tags.js";
2
8
  import { parseToolTag } from "../onboarding/tool-tags.js";
3
9
  import type { CapabilityGapId, EvidencePlanId, TaskFamily } from "../routing/planning.js";
@@ -202,19 +208,6 @@ export const EVIDENCE_PLAN_REGISTRY: Record<EvidencePlanId, EvidencePlanDefiniti
202
208
  ),
203
209
  };
204
210
 
205
- const KNOWN_US_MARKET_HOLIDAYS: Record<string, string> = {
206
- "2026-01-01": "New Year's Day",
207
- "2026-01-19": "Martin Luther King Jr. Day",
208
- "2026-02-16": "Washington's Birthday",
209
- "2026-04-03": "Good Friday",
210
- "2026-05-25": "Memorial Day",
211
- "2026-06-19": "Juneteenth",
212
- "2026-07-03": "Independence Day observed",
213
- "2026-09-07": "Labor Day",
214
- "2026-11-26": "Thanksgiving Day",
215
- "2026-12-25": "Christmas Day",
216
- };
217
-
218
211
  function placeholderPlan(
219
212
  id: EvidencePlanId,
220
213
  taskFamilies: TaskFamily[],
@@ -591,20 +584,6 @@ function temporalReferencesFor(text: string): string[] {
591
584
  return refs;
592
585
  }
593
586
 
594
- function classifyMarketStatus(
595
- local: LocalDateTimeParts,
596
- isWeekend: boolean,
597
- isMarketHoliday: boolean,
598
- temporalReferences: string[],
599
- ): string {
600
- if (isWeekend) return "closed_weekend";
601
- if (isMarketHoliday) return "closed_holiday";
602
- if (local.minutesSinceMidnight < 9 * 60 + 30) return "pre_market";
603
- if (local.minutesSinceMidnight < 16 * 60) return "open";
604
- if (temporalReferences.includes("after_close")) return "after_close";
605
- return "closed_after_hours";
606
- }
607
-
608
587
  function quoteAsOfCaveat(marketStatus: string): string {
609
588
  if (marketStatus === "open")
610
589
  return "Quote evidence may be intraday or delayed depending on provider.";
@@ -613,70 +592,6 @@ function quoteAsOfCaveat(marketStatus: string): string {
613
592
  return "Quote evidence should distinguish the current calendar date from the most recent trading session.";
614
593
  }
615
594
 
616
- interface LocalDateTimeParts {
617
- date: string;
618
- time: string;
619
- weekday: string;
620
- minutesSinceMidnight: number;
621
- }
622
-
623
- function localDateTimeParts(date: Date, timezone: string): LocalDateTimeParts {
624
- const formatter = new Intl.DateTimeFormat("en-US", {
625
- timeZone: timezone,
626
- year: "numeric",
627
- month: "2-digit",
628
- day: "2-digit",
629
- hour: "2-digit",
630
- minute: "2-digit",
631
- hour12: false,
632
- weekday: "short",
633
- });
634
- const parts = formatter.formatToParts(date).reduce<Record<string, string>>((acc, part) => {
635
- acc[part.type] = part.value;
636
- return acc;
637
- }, {});
638
- const hour = Number(parts.hour);
639
- const minute = Number(parts.minute);
640
- return {
641
- date: `${parts.year}-${parts.month}-${parts.day}`,
642
- time: `${parts.hour}:${parts.minute}`,
643
- weekday: parts.weekday ?? "",
644
- minutesSinceMidnight: hour * 60 + minute,
645
- };
646
- }
647
-
648
- function lastTradingDay(localDate: string): string {
649
- let cursor = dateFromKey(localDate);
650
- do {
651
- cursor = addDays(cursor, -1);
652
- } while (isWeekendOrKnownHoliday(dateKey(cursor)));
653
- return dateKey(cursor);
654
- }
655
-
656
- function isWeekendOrKnownHoliday(key: string): boolean {
657
- const date = dateFromKey(key);
658
- const day = date.getUTCDay();
659
- return day === 0 || day === 6 || KNOWN_US_MARKET_HOLIDAYS[key] !== undefined;
660
- }
661
-
662
- function dateFromKey(key: string): Date {
663
- const [year, month, day] = key.split("-").map(Number);
664
- return new Date(Date.UTC(year, month - 1, day));
665
- }
666
-
667
- function addDays(date: Date, days: number): Date {
668
- const next = new Date(date);
669
- next.setUTCDate(next.getUTCDate() + days);
670
- return next;
671
- }
672
-
673
- function dateKey(date: Date): string {
674
- const year = date.getUTCFullYear();
675
- const month = String(date.getUTCMonth() + 1).padStart(2, "0");
676
- const day = String(date.getUTCDate()).padStart(2, "0");
677
- return `${year}-${month}-${day}`;
678
- }
679
-
680
595
  function isRecord(value: unknown): value is Record<string, unknown> {
681
596
  return typeof value === "object" && value !== null && !Array.isArray(value);
682
597
  }
@@ -1,3 +1,6 @@
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import type { FreshnessStamp } from "../infra/freshness.js";
3
+ import type { EvidenceRecord } from "./evidence.js";
1
4
  import type { StepOutput, WorkflowStep } from "./workflow-types.js";
2
5
 
3
6
  /**
@@ -43,14 +46,98 @@ export function promptStep(
43
46
  * Create a StepOutput for a prompt-based step (no structured evidence yet).
44
47
  * Evidence will be captured separately via tool call hooks.
45
48
  */
46
- export function promptStepOutput(stepIndex: number, stepType: string): StepOutput {
49
+ export function promptStepOutput(
50
+ stepIndex: number,
51
+ stepType: string,
52
+ options: { evidence?: EvidenceRecord[]; rawText?: string } = {},
53
+ ): StepOutput {
47
54
  return {
48
55
  stepIndex,
49
56
  stepType,
50
- evidence: [],
57
+ evidence: options.evidence ?? [],
58
+ ...(options.rawText ? { rawText: options.rawText } : {}),
51
59
  };
52
60
  }
53
61
 
62
+ /** Capture compact evidence records from session entries produced by one step. */
63
+ export function captureToolEvidence(entries: SessionEntry[]): EvidenceRecord[] {
64
+ const calls = new Map<string, { tool: string; args: unknown; startedAt: string }>();
65
+ const evidence: EvidenceRecord[] = [];
66
+
67
+ for (const entry of entries) {
68
+ if (entry.type !== "message") continue;
69
+ const message = entry.message as unknown as Record<string, unknown>;
70
+
71
+ if (message.role === "assistant") {
72
+ for (const part of Array.isArray(message.content) ? message.content : []) {
73
+ const record = asRecord(part);
74
+ if (record.type !== "toolCall") continue;
75
+ const id = stringValue(record.id);
76
+ const tool = stringValue(record.name);
77
+ if (!id || !tool) continue;
78
+ calls.set(id, {
79
+ tool,
80
+ args: record.arguments ?? record.args ?? {},
81
+ startedAt: entry.timestamp,
82
+ });
83
+ }
84
+ continue;
85
+ }
86
+
87
+ if (message.role !== "toolResult") continue;
88
+ const toolCallId = stringValue(message.toolCallId);
89
+ const pending = toolCallId ? calls.get(toolCallId) : undefined;
90
+ const tool = pending?.tool ?? stringValue(message.toolName);
91
+ if (!tool) continue;
92
+ const resultValue = message.details ?? message.content ?? "";
93
+ const freshness = extractFreshness(resultValue);
94
+ const serializedResult = serialize(resultValue);
95
+ evidence.push({
96
+ label: `tool:${tool}`,
97
+ value: {
98
+ tool,
99
+ args: truncate(serialize(pending?.args ?? {}), 500),
100
+ ...(freshness ? { freshness } : {}),
101
+ resultDigest: {
102
+ preview: truncate(serializedResult, 500),
103
+ totalLength: serializedResult.length,
104
+ },
105
+ startedAt: pending?.startedAt ?? entry.timestamp,
106
+ completedAt: entry.timestamp,
107
+ },
108
+ provenance: {
109
+ source: "computed",
110
+ timestamp: freshness?.providerDataAt ?? freshness?.fetchedAt ?? entry.timestamp,
111
+ provider: tool,
112
+ confidence: message.isError === true ? 0.5 : undefined,
113
+ },
114
+ });
115
+ }
116
+
117
+ return evidence;
118
+ }
119
+
120
+ /** Extract assistant text emitted during a step, preserving final response order. */
121
+ export function extractAssistantText(entries: SessionEntry[]): string {
122
+ return entries
123
+ .filter((entry) => entry.type === "message")
124
+ .flatMap((entry) => {
125
+ const message = (entry as Extract<SessionEntry, { type: "message" }>)
126
+ .message as unknown as Record<string, unknown>;
127
+ if (message.role !== "assistant") return [];
128
+ const content = message.content;
129
+ if (typeof content === "string") return [content];
130
+ if (!Array.isArray(content)) return [];
131
+ return content
132
+ .map((part) => asRecord(part))
133
+ .filter((part) => part.type === "text")
134
+ .map((part) => stringValue(part.text) ?? "")
135
+ .filter(Boolean);
136
+ })
137
+ .join("\n")
138
+ .trim();
139
+ }
140
+
54
141
  /**
55
142
  * Extract just the WorkflowStep metadata from PromptStep definitions
56
143
  * (dropping the prompt field) for passing to WorkflowRunner.
@@ -58,3 +145,41 @@ export function promptStepOutput(stepIndex: number, stepType: string): StepOutpu
58
145
  export function toStepDefinitions(steps: PromptStep[]): Omit<WorkflowStep, "status">[] {
59
146
  return steps.map(({ prompt: _prompt, ...step }) => step);
60
147
  }
148
+
149
+ function serialize(value: unknown): string {
150
+ if (typeof value === "string") return value;
151
+ try {
152
+ return JSON.stringify(value);
153
+ } catch {
154
+ return String(value);
155
+ }
156
+ }
157
+
158
+ function truncate(value: string, maxLength: number): string {
159
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
160
+ }
161
+
162
+ function asRecord(value: unknown): Record<string, unknown> {
163
+ return typeof value === "object" && value !== null && !Array.isArray(value)
164
+ ? (value as Record<string, unknown>)
165
+ : {};
166
+ }
167
+
168
+ function stringValue(value: unknown): string | undefined {
169
+ return typeof value === "string" && value.length > 0 ? value : undefined;
170
+ }
171
+
172
+ function extractFreshness(value: unknown): FreshnessStamp | undefined {
173
+ const record = asRecord(value);
174
+ return isFreshnessStamp(record.freshness) ? record.freshness : undefined;
175
+ }
176
+
177
+ function isFreshnessStamp(value: unknown): value is FreshnessStamp {
178
+ const record = asRecord(value);
179
+ return (
180
+ typeof record.fetchedAt === "string" &&
181
+ typeof record.cacheStatus === "string" &&
182
+ typeof record.marketSession === "string" &&
183
+ typeof record.isStaleForSession === "boolean"
184
+ );
185
+ }