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.
- package/README.md +5 -4
- package/dist/analysts/orchestrator.d.ts +2 -0
- package/dist/analysts/orchestrator.js +19 -0
- package/dist/analysts/orchestrator.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/summaries.d.ts +8 -0
- package/dist/market-state/summaries.js +70 -0
- package/dist/market-state/summaries.js.map +1 -0
- 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 +44 -5
- package/dist/pi/opencandle-extension.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/entity-extractor.js +16 -0
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/route-manifest.js +1 -1
- 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.d.ts +1 -1
- package/dist/routing/router.js +274 -5
- 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 +352 -49
- 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/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.js +183 -112
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -0
- 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/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.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 +285 -37
- package/gui/server/invoke-tool.ts +14 -13
- package/gui/server/live-chat-event-adapter.ts +37 -2
- package/gui/server/market-state-api.ts +63 -5
- package/gui/server/projector.ts +132 -3
- package/gui/server/session-actions.ts +32 -40
- package/gui/server/session-entry-wait.ts +22 -3
- package/gui/server/ws-hub.ts +16 -4
- 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 +8 -13
- package/src/analysts/orchestrator.ts +20 -0
- 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/summaries.ts +75 -0
- package/src/onboarding/providers.ts +29 -1
- package/src/pi/opencandle-extension.ts +55 -5
- 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/entity-extractor.ts +17 -0
- package/src/routing/route-manifest.ts +1 -1
- package/src/routing/router-llm-client.ts +31 -19
- package/src/routing/router.ts +340 -5
- 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 +462 -57
- 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/tools/fundamentals/comps.ts +25 -14
- package/src/tools/fundamentals/dcf.ts +204 -124
- package/src/tools/index.ts +3 -0
- 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/risk-analysis.ts +1 -1
- 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/gui/server/chat-run-session.ts +0 -16
- package/gui/web/dist/assets/CatalogOverlay-ChRTKNlf.js +0 -1
- package/gui/web/dist/assets/index-BzyqyVnd.css +0 -2
- package/gui/web/dist/assets/index-DvMjkOP9.js +0 -65
|
@@ -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);
|
|
@@ -538,34 +982,11 @@ function buildSavedMarketStateContext(db: Database.Database): string {
|
|
|
538
982
|
];
|
|
539
983
|
|
|
540
984
|
if (lots.length > 0) {
|
|
541
|
-
lines.push(
|
|
542
|
-
for (const lot of lots.slice(0, 8)) {
|
|
543
|
-
const costBasis = formatMoney(lot.quantity * lot.avgCost, lot.currency);
|
|
544
|
-
const name = lot.name ? ` (${lot.name})` : "";
|
|
545
|
-
lines.push(
|
|
546
|
-
`- ${lot.symbol}: ${lot.quantity} @ ${formatMoney(lot.avgCost, lot.currency)}, cost basis ${costBasis}${name}`,
|
|
547
|
-
);
|
|
548
|
-
}
|
|
985
|
+
lines.push(...formatPortfolioSummary(lots));
|
|
549
986
|
}
|
|
550
987
|
|
|
551
988
|
if (watchlist.length > 0) {
|
|
552
|
-
lines.push(
|
|
553
|
-
for (const item of watchlist.slice(0, 8)) {
|
|
554
|
-
const parts = [
|
|
555
|
-
item.targetPrice == null
|
|
556
|
-
? null
|
|
557
|
-
: `target ${formatMoney(item.targetPrice, item.priceCurrency ?? item.currency ?? "USD")}`,
|
|
558
|
-
item.stopPrice == null
|
|
559
|
-
? null
|
|
560
|
-
: `stop ${formatMoney(item.stopPrice, item.priceCurrency ?? item.currency ?? "USD")}`,
|
|
561
|
-
item.thesis ? `thesis: ${item.thesis}` : null,
|
|
562
|
-
item.tags && item.tags.length > 0 ? `tags: ${item.tags.join(", ")}` : null,
|
|
563
|
-
item.notes ? `notes: ${item.notes}` : null,
|
|
564
|
-
].filter((part): part is string => part != null);
|
|
565
|
-
lines.push(
|
|
566
|
-
`- ${item.symbol}${item.name ? ` (${item.name})` : ""}${parts.length > 0 ? ` — ${parts.join("; ")}` : ""}`,
|
|
567
|
-
);
|
|
568
|
-
}
|
|
989
|
+
lines.push(...formatWatchlistSummary(watchlist));
|
|
569
990
|
}
|
|
570
991
|
|
|
571
992
|
if (alerts.length > 0) {
|
|
@@ -589,10 +1010,7 @@ function buildSavedMarketStateContext(db: Database.Database): string {
|
|
|
589
1010
|
}
|
|
590
1011
|
|
|
591
1012
|
if (reportRuns.length > 0) {
|
|
592
|
-
|
|
593
|
-
lines.push(
|
|
594
|
-
`Latest report run: ${latest.status} at ${latest.completedAt ?? latest.startedAt}`,
|
|
595
|
-
);
|
|
1013
|
+
lines.push(...formatLatestReportSummary(reportRuns[0], { includeSummary: false }));
|
|
596
1014
|
}
|
|
597
1015
|
|
|
598
1016
|
return lines.join("\n");
|
|
@@ -614,19 +1032,6 @@ function shouldIncludeSavedMarketStateContext(
|
|
|
614
1032
|
return workflowType != null || fallbackContext != null;
|
|
615
1033
|
}
|
|
616
1034
|
|
|
617
|
-
function formatMoney(value: number, currency: string): string {
|
|
618
|
-
const normalized = currency.toUpperCase();
|
|
619
|
-
if (normalized === "USD") return `$${value.toFixed(2)}`;
|
|
620
|
-
return `${normalized} ${value.toFixed(2)}`;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function formatJsonSummary(value: unknown): string {
|
|
624
|
-
if (value == null) return "";
|
|
625
|
-
const json = JSON.stringify(value);
|
|
626
|
-
if (json.length <= 90) return json;
|
|
627
|
-
return `${json.slice(0, 87)}...`;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
1035
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
631
1036
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
632
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;
|