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.
- package/README.md +12 -9
- package/dist/analysts/orchestrator.d.ts +3 -4
- package/dist/analysts/orchestrator.js +34 -44
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli-main.js +242 -9
- package/dist/cli-main.js.map +1 -1
- package/dist/config.d.ts +4 -9
- package/dist/config.js +7 -10
- package/dist/config.js.map +1 -1
- package/dist/doctor/report.js +1 -1
- package/dist/doctor/report.js.map +1 -1
- package/dist/infra/cache.d.ts +6 -0
- package/dist/infra/cache.js +16 -6
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/freshness.d.ts +21 -0
- package/dist/infra/freshness.js +119 -0
- package/dist/infra/freshness.js.map +1 -0
- package/dist/infra/market-calendar.d.ts +14 -0
- package/dist/infra/market-calendar.js +137 -0
- package/dist/infra/market-calendar.js.map +1 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/daily-report.d.ts +1 -1
- package/dist/market-state/daily-report.js +5 -16
- package/dist/market-state/daily-report.js.map +1 -1
- package/dist/market-state/service.d.ts +0 -35
- package/dist/market-state/service.js +0 -63
- package/dist/market-state/service.js.map +1 -1
- package/dist/market-state/summaries.d.ts +8 -0
- package/dist/market-state/summaries.js +70 -0
- package/dist/market-state/summaries.js.map +1 -0
- package/dist/memory/sqlite.js +20 -19
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/onboarding/providers.d.ts +25 -1
- package/dist/onboarding/providers.js +26 -0
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/pi/opencandle-extension.js +57 -226
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-action-dedupe.d.ts +6 -0
- package/dist/pi/session-action-dedupe.js +126 -0
- package/dist/pi/session-action-dedupe.js.map +1 -0
- package/dist/pi/session-writer-lock.d.ts +26 -2
- package/dist/pi/session-writer-lock.js +230 -18
- package/dist/pi/session-writer-lock.js.map +1 -1
- package/dist/pi/setup.js +5 -5
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tui-session-coordinator.d.ts +15 -0
- package/dist/pi/tui-session-coordinator.js +283 -0
- package/dist/pi/tui-session-coordinator.js.map +1 -0
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +1 -1
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/providers/alpha-vantage.js +6 -0
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -0
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/polymarket.d.ts +2 -0
- package/dist/providers/polymarket.js +163 -0
- package/dist/providers/polymarket.js.map +1 -0
- package/dist/providers/wrap-provider.js +5 -3
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -0
- package/dist/providers/yahoo-finance.js +206 -10
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +4 -5
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/entity-extractor.js +16 -0
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/route-manifest.js +1 -2
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-llm-client.js +18 -3
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router-prompt.js +1 -1
- package/dist/routing/router.d.ts +1 -1
- package/dist/routing/router.js +309 -9
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -0
- package/dist/routing/slot-resolver.js +1 -1
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/numeric-claims.d.ts +23 -0
- package/dist/runtime/numeric-claims.js +100 -0
- package/dist/runtime/numeric-claims.js.map +1 -0
- package/dist/runtime/planning-evidence.js +1 -76
- package/dist/runtime/planning-evidence.js.map +1 -1
- package/dist/runtime/prompt-step.d.ts +10 -1
- package/dist/runtime/prompt-step.js +116 -2
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +11 -1
- package/dist/runtime/session-coordinator.js +354 -62
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/validation.d.ts +3 -1
- package/dist/runtime/workflow-events.d.ts +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-types.d.ts +3 -0
- package/dist/runtime/workflow-types.js.map +1 -1
- package/dist/system-prompt.js +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -0
- package/dist/tools/fundamentals/comps.js +20 -9
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +190 -76
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/macro/event-probabilities.d.ts +8 -0
- package/dist/tools/macro/event-probabilities.js +121 -0
- package/dist/tools/macro/event-probabilities.js.map +1 -0
- package/dist/tools/market/crypto-price.d.ts +4 -1
- package/dist/tools/market/crypto-price.js +10 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.js +14 -3
- package/dist/tools/market/screen-stocks.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +4 -1
- package/dist/tools/market/stock-quote.js +9 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +4 -1
- package/dist/tools/options/option-chain.js +12 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/daily-report.js +10 -4
- package/dist/tools/portfolio/daily-report.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +23 -5
- package/dist/tools/technical/backtest.js +131 -94
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +1 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/fundamentals.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/market.d.ts +2 -0
- package/dist/types/options.d.ts +1 -0
- package/dist/types/prediction-markets.d.ts +13 -0
- package/dist/types/prediction-markets.js +2 -0
- package/dist/types/prediction-markets.js.map +1 -0
- package/gui/server/chat-event-adapter.ts +89 -5
- package/gui/server/http-routes.ts +661 -37
- package/gui/server/invoke-tool.ts +165 -16
- package/gui/server/live-chat-event-adapter.ts +37 -2
- package/gui/server/local-session-coordinator.ts +97 -0
- package/gui/server/market-state-api.ts +64 -48
- package/gui/server/projector.ts +132 -3
- package/gui/server/server.ts +51 -6
- package/gui/server/session-actions.ts +146 -45
- package/gui/server/session-entry-wait.ts +22 -3
- package/gui/server/tool-metadata.ts +3 -1
- package/gui/server/ws-hub.ts +77 -10
- package/gui/shared/chat-events.ts +10 -1
- package/gui/shared/event-reducer.ts +1 -0
- package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
- package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
- package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +13 -15
- package/src/analysts/orchestrator.ts +35 -56
- package/src/cli-main.ts +253 -13
- package/src/config.ts +12 -20
- package/src/doctor/report.ts +1 -1
- package/src/infra/cache.ts +24 -11
- package/src/infra/freshness.ts +165 -0
- package/src/infra/market-calendar.ts +193 -0
- package/src/infra/rate-limiter.ts +1 -0
- package/src/market-state/daily-report.ts +6 -16
- package/src/market-state/service.ts +0 -136
- package/src/market-state/summaries.ts +75 -0
- package/src/memory/sqlite.ts +23 -19
- package/src/onboarding/providers.ts +29 -1
- package/src/pi/opencandle-extension.ts +67 -270
- package/src/pi/session-action-dedupe.ts +155 -0
- package/src/pi/session-writer-lock.ts +290 -20
- package/src/pi/setup.ts +6 -6
- package/src/pi/tui-session-coordinator.ts +351 -0
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +1 -1
- package/src/providers/alpha-vantage.ts +6 -0
- package/src/providers/coingecko.ts +2 -0
- package/src/providers/polymarket.ts +214 -0
- package/src/providers/wrap-provider.ts +5 -3
- package/src/providers/yahoo-finance.ts +339 -27
- package/src/routing/classify-intent.ts +4 -5
- package/src/routing/entity-extractor.ts +17 -0
- package/src/routing/route-manifest.ts +1 -2
- package/src/routing/router-llm-client.ts +31 -19
- package/src/routing/router-prompt.ts +1 -1
- package/src/routing/router.ts +379 -8
- package/src/routing/slot-resolver.ts +1 -1
- package/src/runtime/evidence.ts +1 -0
- package/src/runtime/numeric-claims.ts +108 -0
- package/src/runtime/planning-evidence.ts +6 -91
- package/src/runtime/prompt-step.ts +127 -2
- package/src/runtime/session-coordinator.ts +464 -74
- package/src/runtime/validation.ts +1 -1
- package/src/runtime/workflow-events.ts +2 -0
- package/src/runtime/workflow-types.ts +3 -0
- package/src/system-prompt.ts +1 -1
- package/src/tools/AGENTS.md +1 -1
- package/src/tools/fundamentals/comps.ts +25 -14
- package/src/tools/fundamentals/dcf.ts +217 -87
- package/src/tools/index.ts +3 -3
- package/src/tools/macro/event-probabilities.ts +141 -0
- package/src/tools/market/crypto-price.ts +14 -2
- package/src/tools/market/screen-stocks.ts +14 -3
- package/src/tools/market/stock-quote.ts +49 -43
- package/src/tools/options/option-chain.ts +16 -2
- package/src/tools/portfolio/daily-report.ts +10 -4
- package/src/tools/portfolio/risk-analysis.ts +1 -1
- package/src/tools/technical/backtest.ts +167 -108
- package/src/tools/technical/indicators.ts +1 -1
- package/src/types/fundamentals.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/market.ts +2 -0
- package/src/types/options.ts +1 -0
- package/src/types/prediction-markets.ts +13 -0
- package/dist/tools/portfolio/predictions.d.ts +0 -55
- package/dist/tools/portfolio/predictions.js +0 -422
- package/dist/tools/portfolio/predictions.js.map +0 -1
- package/gui/server/chat-run-session.ts +0 -16
- package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
- package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
- package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
- 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 {
|
|
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(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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;
|