opencandle 0.3.0 → 0.4.0
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/assets/logo.svg +187 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +38 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/infra/browser.d.ts +10 -0
- package/dist/infra/browser.js +1 -0
- package/dist/infra/browser.js.map +1 -1
- package/dist/infra/native-dependencies.d.ts +1 -0
- package/dist/infra/native-dependencies.js +10 -0
- package/dist/infra/native-dependencies.js.map +1 -0
- package/dist/infra/node-version.d.ts +2 -0
- package/dist/infra/node-version.js +23 -0
- package/dist/infra/node-version.js.map +1 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/sqlite.js +42 -4
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/storage.d.ts +6 -0
- package/dist/memory/storage.js +3 -3
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/tool-defaults.d.ts +8 -0
- package/dist/memory/tool-defaults.js +59 -0
- package/dist/memory/tool-defaults.js.map +1 -0
- package/dist/onboarding/connect.d.ts +13 -1
- package/dist/onboarding/connect.js +21 -10
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/prompt-user.d.ts +1 -1
- package/dist/onboarding/providers.d.ts +7 -0
- package/dist/onboarding/providers.js +6 -3
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/tool-helpers.d.ts +1 -1
- package/dist/pi/opencandle-extension.d.ts +7 -1
- package/dist/pi/opencandle-extension.js +186 -10
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-storage.d.ts +2 -0
- package/dist/pi/session-storage.js +5 -0
- package/dist/pi/session-storage.js.map +1 -0
- package/dist/pi/session.d.ts +4 -1
- package/dist/pi/session.js +25 -3
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/setup.d.ts +1 -1
- package/dist/pi/setup.js +1 -1
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +2 -2
- package/dist/pi/tool-adapter.js +14 -1
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +22 -0
- package/dist/prompts/context-builder.js +45 -10
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/prompts/disclaimer.d.ts +6 -0
- package/dist/prompts/disclaimer.js +9 -0
- package/dist/prompts/disclaimer.js.map +1 -0
- package/dist/prompts/workflow-prompts.d.ts +8 -0
- package/dist/prompts/workflow-prompts.js +39 -5
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/yahoo-finance.js +70 -33
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/defaults.js +1 -1
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +3 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/router-llm-client.d.ts +11 -0
- package/dist/routing/router-llm-client.js +42 -0
- package/dist/routing/router-llm-client.js.map +1 -0
- package/dist/routing/router-prompt.d.ts +2 -0
- package/dist/routing/router-prompt.js +138 -0
- package/dist/routing/router-prompt.js.map +1 -0
- package/dist/routing/router-types.d.ts +62 -0
- package/dist/routing/router-types.js +2 -0
- package/dist/routing/router-types.js.map +1 -0
- package/dist/routing/router.d.ts +10 -0
- package/dist/routing/router.js +194 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/runtime/session-coordinator.d.ts +63 -3
- package/dist/runtime/session-coordinator.js +155 -4
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
- package/dist/runtime/tool-defaults-wrapper.js +25 -0
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
- package/dist/sentiment/store.js +5 -0
- package/dist/sentiment/store.js.map +1 -1
- package/dist/system-prompt.js +20 -12
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +4 -4
- package/dist/tools/fundamentals/company-overview.d.ts +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +1 -1
- package/dist/tools/fundamentals/financials.d.ts +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +1 -1
- package/dist/tools/index.d.ts +28 -1
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.d.ts +1 -1
- package/dist/tools/interaction/twitter-login.d.ts +1 -1
- package/dist/tools/macro/fear-greed.d.ts +1 -1
- package/dist/tools/macro/fred-data.d.ts +1 -1
- package/dist/tools/market/crypto-history.d.ts +1 -1
- package/dist/tools/market/crypto-price.d.ts +1 -1
- package/dist/tools/market/search-ticker.d.ts +1 -1
- package/dist/tools/market/stock-history.d.ts +1 -1
- package/dist/tools/market/stock-quote.d.ts +1 -1
- package/dist/tools/options/option-chain.d.ts +1 -1
- package/dist/tools/options/option-chain.js +4 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/predictions.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/tools/portfolio/tracker.d.ts +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +1 -1
- package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/web-search.d.ts +1 -1
- package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
- package/dist/tools/technical/backtest.d.ts +1 -1
- package/dist/tools/technical/indicators.d.ts +1 -1
- package/dist/tools/technical/indicators.js +7 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/workflows/options-screener.js +7 -2
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.js +3 -3
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/background-quotes.ts +31 -0
- package/gui/server/chat-event-adapter.ts +142 -0
- package/gui/server/invoke-tool.ts +89 -0
- package/gui/server/live-chat-event-adapter.ts +181 -0
- package/gui/server/model-setup.ts +100 -0
- package/gui/server/package.json +5 -0
- package/gui/server/projector.ts +212 -0
- package/gui/server/server.ts +592 -0
- package/gui/server/session-actions.ts +31 -0
- package/gui/server/tool-metadata.ts +88 -0
- package/gui/server/websocket.ts +128 -0
- package/gui/server/writer-lock.ts +118 -0
- package/gui/shared/chat-events.ts +118 -0
- package/gui/shared/event-reducer.ts +186 -0
- package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +1 -0
- package/gui/web/dist/assets/index-DBrWq43L.css +1 -0
- package/gui/web/dist/assets/index-RflHaj0y.js +67 -0
- package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
- package/gui/web/dist/index.html +17 -0
- package/package.json +44 -18
- package/src/analysts/contracts.ts +189 -0
- package/src/analysts/orchestrator.ts +300 -0
- package/src/cli.ts +205 -0
- package/src/config.ts +161 -0
- package/src/index.ts +5 -0
- package/src/infra/browser.ts +111 -0
- package/src/infra/cache.ts +103 -0
- package/src/infra/http-client.ts +68 -0
- package/src/infra/index.ts +18 -0
- package/src/infra/native-dependencies.ts +12 -0
- package/src/infra/node-version.ts +24 -0
- package/src/infra/open-url.ts +28 -0
- package/src/infra/opencandle-paths.ts +64 -0
- package/src/infra/rate-limiter.ts +64 -0
- package/src/memory/index.ts +10 -0
- package/src/memory/manager.ts +159 -0
- package/src/memory/preference-extractor.ts +106 -0
- package/src/memory/retrieval.ts +70 -0
- package/src/memory/sqlite.ts +172 -0
- package/src/memory/storage.ts +204 -0
- package/src/memory/tool-defaults.ts +87 -0
- package/src/memory/types.ts +67 -0
- package/src/onboarding/connect.ts +184 -0
- package/src/onboarding/credential-interceptor.ts +134 -0
- package/src/onboarding/degradation-accumulator.ts +79 -0
- package/src/onboarding/prompt-user.ts +85 -0
- package/src/onboarding/providers.ts +315 -0
- package/src/onboarding/state.ts +218 -0
- package/src/onboarding/tool-helpers.ts +111 -0
- package/src/onboarding/tool-tags.ts +201 -0
- package/src/onboarding/validation.ts +158 -0
- package/src/pi/opencandle-extension.ts +724 -0
- package/src/pi/session-storage.ts +5 -0
- package/src/pi/session.ts +81 -0
- package/src/pi/setup.ts +371 -0
- package/src/pi/tool-adapter.ts +36 -0
- package/src/prompts/context-builder.ts +204 -0
- package/src/prompts/disclaimer.ts +9 -0
- package/src/prompts/sections.ts +46 -0
- package/src/prompts/workflow-prompts.ts +279 -0
- package/src/providers/alpha-vantage.ts +292 -0
- package/src/providers/coingecko.ts +96 -0
- package/src/providers/exa-search.ts +373 -0
- package/src/providers/fear-greed.ts +45 -0
- package/src/providers/finnhub.ts +124 -0
- package/src/providers/fred.ts +83 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider-credential-error.ts +23 -0
- package/src/providers/reddit.ts +151 -0
- package/src/providers/sec-edgar.ts +96 -0
- package/src/providers/twitter.ts +173 -0
- package/src/providers/web-search.ts +293 -0
- package/src/providers/with-fallback.ts +41 -0
- package/src/providers/wrap-provider.ts +64 -0
- package/src/providers/yahoo-finance.ts +367 -0
- package/src/routing/classify-intent.ts +194 -0
- package/src/routing/defaults.ts +29 -0
- package/src/routing/entity-extractor.ts +140 -0
- package/src/routing/index.ts +26 -0
- package/src/routing/router-llm-client.ts +51 -0
- package/src/routing/router-prompt.ts +159 -0
- package/src/routing/router-types.ts +66 -0
- package/src/routing/router.ts +213 -0
- package/src/routing/slot-resolver.ts +152 -0
- package/src/routing/types.ts +63 -0
- package/src/runtime/evidence.ts +77 -0
- package/src/runtime/index.ts +55 -0
- package/src/runtime/prompt-step.ts +75 -0
- package/src/runtime/provider-ids.ts +15 -0
- package/src/runtime/provider-tracker.ts +40 -0
- package/src/runtime/run-context.ts +22 -0
- package/src/runtime/session-coordinator.ts +406 -0
- package/src/runtime/tool-defaults-wrapper.ts +35 -0
- package/src/runtime/validation.ts +214 -0
- package/src/runtime/workflow-events.ts +75 -0
- package/src/runtime/workflow-runner.ts +188 -0
- package/src/runtime/workflow-types.ts +102 -0
- package/src/sentiment/adapters/finnhub.ts +44 -0
- package/src/sentiment/adapters/reddit.ts +65 -0
- package/src/sentiment/adapters/twitter.ts +36 -0
- package/src/sentiment/adapters/web.ts +44 -0
- package/src/sentiment/index.ts +58 -0
- package/src/sentiment/keywords.ts +9 -0
- package/src/sentiment/pipeline.ts +68 -0
- package/src/sentiment/scorer.ts +78 -0
- package/src/sentiment/store.ts +260 -0
- package/src/sentiment/trends.ts +90 -0
- package/src/sentiment/types.ts +108 -0
- package/src/system-prompt.ts +115 -0
- package/src/tool-kit.ts +68 -0
- package/src/tools/AGENTS.md +36 -0
- package/src/tools/fundamentals/company-overview.ts +54 -0
- package/src/tools/fundamentals/comps.ts +156 -0
- package/src/tools/fundamentals/dcf.ts +267 -0
- package/src/tools/fundamentals/earnings.ts +47 -0
- package/src/tools/fundamentals/financials.ts +54 -0
- package/src/tools/fundamentals/sec-filings.ts +61 -0
- package/src/tools/index.ts +88 -0
- package/src/tools/interaction/ask-user.ts +81 -0
- package/src/tools/interaction/twitter-login.ts +93 -0
- package/src/tools/macro/fear-greed.ts +41 -0
- package/src/tools/macro/fred-data.ts +54 -0
- package/src/tools/market/crypto-history.ts +51 -0
- package/src/tools/market/crypto-price.ts +53 -0
- package/src/tools/market/search-ticker.ts +53 -0
- package/src/tools/market/stock-history.ts +79 -0
- package/src/tools/market/stock-quote.ts +64 -0
- package/src/tools/options/greeks.ts +82 -0
- package/src/tools/options/option-chain.ts +91 -0
- package/src/tools/portfolio/correlation.ts +162 -0
- package/src/tools/portfolio/predictions.ts +253 -0
- package/src/tools/portfolio/risk-analysis.ts +134 -0
- package/src/tools/portfolio/tracker.ts +147 -0
- package/src/tools/portfolio/watchlist.ts +153 -0
- package/src/tools/sentiment/reddit-sentiment.ts +164 -0
- package/src/tools/sentiment/sentiment-summary.ts +256 -0
- package/src/tools/sentiment/sentiment-trend.ts +58 -0
- package/src/tools/sentiment/twitter-sentiment.ts +96 -0
- package/src/tools/sentiment/web-search.ts +150 -0
- package/src/tools/sentiment/web-sentiment.ts +76 -0
- package/src/tools/technical/backtest.ts +246 -0
- package/src/tools/technical/indicators.ts +258 -0
- package/src/types/fundamentals.ts +46 -0
- package/src/types/index.ts +20 -0
- package/src/types/macro.ts +27 -0
- package/src/types/market.ts +43 -0
- package/src/types/options.ts +35 -0
- package/src/types/portfolio.ts +41 -0
- package/src/types/sentiment.ts +70 -0
- package/src/workflows/compare-assets.ts +39 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/options-screener.ts +49 -0
- package/src/workflows/portfolio-builder.ts +52 -0
- package/src/workflows/types.ts +4 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getAllDefaults, initDefaultDatabase, MemoryStorage } from "../memory/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Alias for the session-manager handle extensions receive via
|
|
6
|
+
* `ExtensionContext`. `ReadonlySessionManager` is defined inside pi-coding-
|
|
7
|
+
* agent but is not re-exported from the package's `.` entry, so we derive
|
|
8
|
+
* the shape we need from the public `ExtensionContext` type.
|
|
9
|
+
*/
|
|
10
|
+
type ReadonlySessionManager = ExtensionContext["sessionManager"];
|
|
11
|
+
import { MemoryManager } from "../memory/manager.js";
|
|
12
|
+
import { extractPreferences } from "../memory/preference-extractor.js";
|
|
13
|
+
import { runOpenCandleSetup } from "../pi/setup.js";
|
|
14
|
+
import { WorkflowEventLogger } from "./workflow-events.js";
|
|
15
|
+
import { ProviderTracker } from "./provider-tracker.js";
|
|
16
|
+
import { WorkflowRunner } from "./workflow-runner.js";
|
|
17
|
+
import { setRunContext, clearRunContext } from "./run-context.js";
|
|
18
|
+
import { PromptContextBuilder, type FallbackContext } from "../prompts/context-builder.js";
|
|
19
|
+
import { getAddonToolDescriptions } from "../tool-kit.js";
|
|
20
|
+
import type { WorkflowDefinition } from "./prompt-step.js";
|
|
21
|
+
import { toStepDefinitions, promptStepOutput } from "./prompt-step.js";
|
|
22
|
+
import type Database from "better-sqlite3";
|
|
23
|
+
|
|
24
|
+
const PROMPT_SETTLE_POLL_MS = 25;
|
|
25
|
+
const IMMEDIATE_IDLE_GRACE_MS = 100;
|
|
26
|
+
|
|
27
|
+
function parseMaybeJson(raw: unknown): Record<string, unknown> | undefined {
|
|
28
|
+
if (typeof raw !== "string" || raw.length === 0) return undefined;
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
32
|
+
? (parsed as Record<string, unknown>)
|
|
33
|
+
: undefined;
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type QueueContext = ExtensionCommandContext | {
|
|
40
|
+
isIdle(): boolean;
|
|
41
|
+
hasPendingMessages?(): boolean;
|
|
42
|
+
ui?: { notify(message: string, level?: string): void };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function hasPendingMessages(ctx: QueueContext): boolean {
|
|
46
|
+
return ctx.hasPendingMessages?.() ?? false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isReadyForNextPrompt(ctx: QueueContext): boolean {
|
|
50
|
+
return ctx.isIdle() && !hasPendingMessages(ctx);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sleep(ms: number): Promise<void> {
|
|
54
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function waitForPromptSettlement(
|
|
58
|
+
ctx: QueueContext,
|
|
59
|
+
isCurrentRun: () => boolean,
|
|
60
|
+
): Promise<boolean> {
|
|
61
|
+
let sawBusyOrPending = !isReadyForNextPrompt(ctx);
|
|
62
|
+
const startedAt = Date.now();
|
|
63
|
+
|
|
64
|
+
while (isCurrentRun()) {
|
|
65
|
+
const ready = isReadyForNextPrompt(ctx);
|
|
66
|
+
if (!ready) {
|
|
67
|
+
sawBusyOrPending = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (sawBusyOrPending && ready) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!sawBusyOrPending && ready && Date.now() - startedAt >= IMMEDIATE_IDLE_GRACE_MS) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await sleep(PROMPT_SETTLE_POLL_MS);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Coordinates session lifecycle, memory, workflow execution,
|
|
86
|
+
* and prompt assembly. The extension delegates to this.
|
|
87
|
+
*/
|
|
88
|
+
export class SessionCoordinator {
|
|
89
|
+
private db: Database.Database | null = null;
|
|
90
|
+
private storage: MemoryStorage | null = null;
|
|
91
|
+
private memoryManager: MemoryManager | null = null;
|
|
92
|
+
private eventLogger: WorkflowEventLogger | null = null;
|
|
93
|
+
private runner: WorkflowRunner;
|
|
94
|
+
private providerTracker: ProviderTracker;
|
|
95
|
+
private sessionId = "unknown";
|
|
96
|
+
|
|
97
|
+
constructor() {
|
|
98
|
+
// Runner is always available — event logger is optional and added after session init
|
|
99
|
+
this.providerTracker = new ProviderTracker();
|
|
100
|
+
this.runner = new WorkflowRunner({ providerTracker: this.providerTracker });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getStorage(): MemoryStorage | null {
|
|
104
|
+
return this.storage;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getRunner(): WorkflowRunner {
|
|
108
|
+
return this.runner;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Initialize session: database, memory, event logger, workflow runner. */
|
|
112
|
+
initSession(sessionId: string): void {
|
|
113
|
+
this.db = initDefaultDatabase();
|
|
114
|
+
this.storage = new MemoryStorage(this.db);
|
|
115
|
+
this.memoryManager = new MemoryManager(this.storage);
|
|
116
|
+
this.eventLogger = new WorkflowEventLogger(this.db);
|
|
117
|
+
this.providerTracker = new ProviderTracker();
|
|
118
|
+
this.runner = new WorkflowRunner({
|
|
119
|
+
eventLogger: this.eventLogger,
|
|
120
|
+
providerTracker: this.providerTracker,
|
|
121
|
+
});
|
|
122
|
+
this.sessionId = sessionId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Run setup flow. */
|
|
126
|
+
async runSetup(
|
|
127
|
+
pi: ExtensionAPI,
|
|
128
|
+
ctx: ExtensionContext | ExtensionCommandContext,
|
|
129
|
+
options: { mode: "startup" | "manual" },
|
|
130
|
+
): Promise<"ready" | "shutdown" | "cancelled"> {
|
|
131
|
+
return runOpenCandleSetup(pi, ctx, options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Extract and persist user preferences from natural language. */
|
|
135
|
+
extractAndStorePreferences(text: string): void {
|
|
136
|
+
if (!this.storage) return;
|
|
137
|
+
for (const pref of extractPreferences(text)) {
|
|
138
|
+
this.storage.upsertPreference({
|
|
139
|
+
key: pref.key,
|
|
140
|
+
valueJson: JSON.stringify(pref.value),
|
|
141
|
+
confidence: pref.confidence,
|
|
142
|
+
source: "inferred",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Record a workflow run in storage. */
|
|
148
|
+
recordWorkflowRun(
|
|
149
|
+
workflowType: string,
|
|
150
|
+
entities: object,
|
|
151
|
+
resolved: object,
|
|
152
|
+
defaultsUsed: unknown[],
|
|
153
|
+
turnType: "workflow" | "fallback" = "workflow",
|
|
154
|
+
): void {
|
|
155
|
+
this.storage?.insertWorkflowRun({
|
|
156
|
+
sessionId: this.sessionId,
|
|
157
|
+
workflowType,
|
|
158
|
+
inputSlotsJson: JSON.stringify(entities),
|
|
159
|
+
resolvedSlotsJson: JSON.stringify(resolved),
|
|
160
|
+
defaultsUsedJson: JSON.stringify(defaultsUsed),
|
|
161
|
+
turnType,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract the last `max` user/assistant turns from the session branch as
|
|
167
|
+
* `{role, text}` pairs, oldest→newest. Walks `sessionManager.getBranch()`
|
|
168
|
+
* (root→leaf order) and filters per the intent-routing spec:
|
|
169
|
+
*
|
|
170
|
+
* - Keep only `type === "message"` entries whose `message.role` is
|
|
171
|
+
* `"user"` or `"assistant"`. Compaction, branch_summary, custom, label,
|
|
172
|
+
* thinking_level_change, model_change, and session_info entries are
|
|
173
|
+
* skipped. Tool-result messages (`role === "toolResult"`) are skipped.
|
|
174
|
+
* - Extract concatenated text-block content. User `content` may be a
|
|
175
|
+
* plain string; assistant `content` is always an array of blocks, from
|
|
176
|
+
* which only `type === "text"` blocks contribute.
|
|
177
|
+
* - Drop entries whose resulting text is empty or whitespace-only
|
|
178
|
+
* (handles aborted assistant turns and tool-only assistant turns).
|
|
179
|
+
* - Slice to the last `max` qualifying entries.
|
|
180
|
+
*
|
|
181
|
+
* Privacy note: conversational text in priorTurns is NOT filtered by
|
|
182
|
+
* `NEVER_TRUST_FROM_MEMORY` (which governs structured memory keys). A
|
|
183
|
+
* future `/forget` command is the designated scrubbing primitive — see
|
|
184
|
+
* `openspec/changes/router-context-and-observability/` for the follow-up.
|
|
185
|
+
*/
|
|
186
|
+
buildPriorTurns(
|
|
187
|
+
sessionManager: ReadonlySessionManager,
|
|
188
|
+
max = 5,
|
|
189
|
+
): Array<{ role: "user" | "assistant"; text: string }> {
|
|
190
|
+
const branch = sessionManager.getBranch();
|
|
191
|
+
const turns: Array<{ role: "user" | "assistant"; text: string }> = [];
|
|
192
|
+
|
|
193
|
+
for (const entry of branch) {
|
|
194
|
+
if (entry.type !== "message") continue;
|
|
195
|
+
const msg = (entry as SessionEntry & { type: "message" }).message;
|
|
196
|
+
if (!msg || typeof msg !== "object") continue;
|
|
197
|
+
const role = (msg as { role?: unknown }).role;
|
|
198
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
199
|
+
|
|
200
|
+
const rawContent = (msg as { content?: unknown }).content;
|
|
201
|
+
let text = "";
|
|
202
|
+
if (typeof rawContent === "string") {
|
|
203
|
+
text = rawContent;
|
|
204
|
+
} else if (Array.isArray(rawContent)) {
|
|
205
|
+
for (const block of rawContent) {
|
|
206
|
+
if (
|
|
207
|
+
block &&
|
|
208
|
+
typeof block === "object" &&
|
|
209
|
+
(block as { type?: unknown }).type === "text" &&
|
|
210
|
+
typeof (block as { text?: unknown }).text === "string"
|
|
211
|
+
) {
|
|
212
|
+
text += (block as { text: string }).text;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (text.trim().length === 0) continue;
|
|
218
|
+
turns.push({ role, text });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return turns.slice(-max);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Expose prior turns + recent runs + profile snapshot for the router
|
|
226
|
+
* input context. Fixed-window per design.md §7 (last 5 turns, 3 runs).
|
|
227
|
+
* `priorTurns` is derived from the session branch at call time; see
|
|
228
|
+
* `buildPriorTurns` for the filter rules.
|
|
229
|
+
*/
|
|
230
|
+
buildRouterContextBase(sessionManager: ReadonlySessionManager): {
|
|
231
|
+
profileSnapshot: Record<string, unknown>;
|
|
232
|
+
recentWorkflowRuns: Array<{
|
|
233
|
+
workflowType: string;
|
|
234
|
+
turnType: string;
|
|
235
|
+
resolvedSlots?: Record<string, unknown>;
|
|
236
|
+
createdAt: string;
|
|
237
|
+
}>;
|
|
238
|
+
priorTurns: Array<{ role: "user" | "assistant"; text: string }>;
|
|
239
|
+
} {
|
|
240
|
+
const priorTurns = this.buildPriorTurns(sessionManager);
|
|
241
|
+
if (!this.storage) {
|
|
242
|
+
return { profileSnapshot: {}, recentWorkflowRuns: [], priorTurns };
|
|
243
|
+
}
|
|
244
|
+
const prefs = this.storage.getPreferencesByNamespace("global");
|
|
245
|
+
const profileSnapshot: Record<string, unknown> = {};
|
|
246
|
+
for (const p of prefs) {
|
|
247
|
+
const key = String(p.key);
|
|
248
|
+
const rawValue = p.value_json;
|
|
249
|
+
if (typeof rawValue === "string") {
|
|
250
|
+
try {
|
|
251
|
+
profileSnapshot[key] = JSON.parse(rawValue);
|
|
252
|
+
} catch {
|
|
253
|
+
profileSnapshot[key] = rawValue;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const runs = this.storage.getRecentWorkflowRuns(3).map((r) => ({
|
|
258
|
+
workflowType: String(r.workflow_type ?? ""),
|
|
259
|
+
turnType: String(r.turn_type ?? "workflow"),
|
|
260
|
+
resolvedSlots: parseMaybeJson(r.resolved_slots_json),
|
|
261
|
+
createdAt: String(r.created_at ?? ""),
|
|
262
|
+
}));
|
|
263
|
+
return { profileSnapshot, recentWorkflowRuns: runs, priorTurns };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Build system prompt using composable sections. */
|
|
267
|
+
buildSystemPrompt(
|
|
268
|
+
basePrompt: string,
|
|
269
|
+
workflowType?: string,
|
|
270
|
+
fallbackContext?: FallbackContext,
|
|
271
|
+
): string {
|
|
272
|
+
const builder = new PromptContextBuilder();
|
|
273
|
+
|
|
274
|
+
const addonTools = getAddonToolDescriptions();
|
|
275
|
+
const addonDescriptions = addonTools.length > 0
|
|
276
|
+
? addonTools.map((t) => `${t.name}: ${t.description}`)
|
|
277
|
+
: undefined;
|
|
278
|
+
|
|
279
|
+
const memoryContext = this.memoryManager
|
|
280
|
+
? this.memoryManager.buildContext(workflowType ?? "unclassified")
|
|
281
|
+
: undefined;
|
|
282
|
+
|
|
283
|
+
builder.populateFromOptions({
|
|
284
|
+
workflowType,
|
|
285
|
+
memoryContext: memoryContext || undefined,
|
|
286
|
+
addonToolDescriptions: addonDescriptions,
|
|
287
|
+
fallbackContext,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const toolDefaults = formatToolDefaultsForPrompt();
|
|
291
|
+
const defaultsSection = toolDefaults.length > 0
|
|
292
|
+
? `\n\n## User Tool Defaults\n${toolDefaults.join("\n")}`
|
|
293
|
+
: "";
|
|
294
|
+
|
|
295
|
+
return `${basePrompt}\n\n${builder.build()}${defaultsSection}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Stash a pending fallback context so the very next `before_agent_start`
|
|
300
|
+
* hook can slot it into the system prompt. Cleared after consumption so
|
|
301
|
+
* subsequent turns do not inherit stale fallback directives.
|
|
302
|
+
*/
|
|
303
|
+
private pendingFallbackContext: FallbackContext | null = null;
|
|
304
|
+
|
|
305
|
+
setPendingFallbackContext(ctx: FallbackContext | null): void {
|
|
306
|
+
this.pendingFallbackContext = ctx;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
consumePendingFallbackContext(): FallbackContext | null {
|
|
310
|
+
const ctx = this.pendingFallbackContext;
|
|
311
|
+
this.pendingFallbackContext = null;
|
|
312
|
+
return ctx;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Execute a workflow definition through the WorkflowRunner,
|
|
317
|
+
* sending prompts via Pi with settlement-based sequencing.
|
|
318
|
+
*/
|
|
319
|
+
executeWorkflow(
|
|
320
|
+
pi: ExtensionAPI,
|
|
321
|
+
definition: WorkflowDefinition,
|
|
322
|
+
ctx: QueueContext,
|
|
323
|
+
): void {
|
|
324
|
+
if (definition.steps.length === 0) return;
|
|
325
|
+
|
|
326
|
+
const runner = this.runner;
|
|
327
|
+
const runRef = { active: true };
|
|
328
|
+
|
|
329
|
+
// Send the first prompt immediately
|
|
330
|
+
const [firstStep, ...restSteps] = definition.steps;
|
|
331
|
+
const startedBusy = !isReadyForNextPrompt(ctx);
|
|
332
|
+
|
|
333
|
+
if (startedBusy) {
|
|
334
|
+
pi.sendUserMessage(firstStep.prompt, { deliverAs: "followUp" });
|
|
335
|
+
ctx.ui?.notify?.("Analysis queued as follow-up.", "info");
|
|
336
|
+
} else {
|
|
337
|
+
pi.sendUserMessage(firstStep.prompt);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Make the run's ProviderTracker accessible to tools during execution
|
|
341
|
+
setRunContext({ providerTracker: this.providerTracker });
|
|
342
|
+
|
|
343
|
+
// Start the runner in the background for state tracking
|
|
344
|
+
const stepDefs = toStepDefinitions(definition.steps);
|
|
345
|
+
void runner.start(definition.workflowType, stepDefs, async (step, stepIndex) => {
|
|
346
|
+
// First step was already sent above — just wait for settlement
|
|
347
|
+
if (stepIndex > 0) {
|
|
348
|
+
const settled = await waitForPromptSettlement(ctx, () => runRef.active);
|
|
349
|
+
if (!settled || !runRef.active) {
|
|
350
|
+
throw new Error("run_cancelled");
|
|
351
|
+
}
|
|
352
|
+
pi.sendUserMessage(definition.steps[stepIndex].prompt);
|
|
353
|
+
} else {
|
|
354
|
+
// For the first step, just wait for it to settle
|
|
355
|
+
const settled = await waitForPromptSettlement(ctx, () => runRef.active);
|
|
356
|
+
if (!settled || !runRef.active) {
|
|
357
|
+
throw new Error("run_cancelled");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return promptStepOutput(stepIndex, step.stepType);
|
|
361
|
+
}).finally(() => {
|
|
362
|
+
clearRunContext();
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Cancel any active workflow. */
|
|
367
|
+
cancelActiveWorkflow(): void {
|
|
368
|
+
clearRunContext();
|
|
369
|
+
this.runner?.cancel();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function formatToolDefaultsForPrompt(): string[] {
|
|
374
|
+
try {
|
|
375
|
+
return [...getAllDefaults()]
|
|
376
|
+
.filter(([, defaults]) => Object.keys(defaults).some((key) => key !== "__enabled"))
|
|
377
|
+
.map(([toolName, defaults]) => {
|
|
378
|
+
const pairs = flattenDefaults(defaults)
|
|
379
|
+
.filter(([key]) => key !== "__enabled")
|
|
380
|
+
.map(([key, value]) => `${key}: ${String(value)}`);
|
|
381
|
+
return `- User has set defaults for \`${toolName}\` (${pairs.join(", ")}). You may override when the user's request requires it.`;
|
|
382
|
+
});
|
|
383
|
+
} catch {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function flattenDefaults(
|
|
389
|
+
defaults: Record<string, unknown>,
|
|
390
|
+
prefix = "",
|
|
391
|
+
): Array<[string, unknown]> {
|
|
392
|
+
const out: Array<[string, unknown]> = [];
|
|
393
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
394
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
395
|
+
if (isPlainObject(value)) {
|
|
396
|
+
out.push(...flattenDefaults(value, path));
|
|
397
|
+
} else {
|
|
398
|
+
out.push([path, value]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
405
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
406
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
3
|
+
|
|
4
|
+
export function wrapWithDefaults<TParams extends TSchema, TDetails>(
|
|
5
|
+
tool: AgentTool<TParams, TDetails>,
|
|
6
|
+
defaults: Record<string, unknown>,
|
|
7
|
+
): AgentTool<TParams, TDetails> {
|
|
8
|
+
return {
|
|
9
|
+
...tool,
|
|
10
|
+
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
11
|
+
const merged = mergeDefaults(defaults, params as Record<string, unknown>);
|
|
12
|
+
return tool.execute(toolCallId, merged as typeof params, signal, onUpdate);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function mergeDefaults(
|
|
18
|
+
defaults: Record<string, unknown>,
|
|
19
|
+
args: Record<string, unknown>,
|
|
20
|
+
): Record<string, unknown> {
|
|
21
|
+
if (Object.keys(defaults).length === 0) return args;
|
|
22
|
+
|
|
23
|
+
const out: Record<string, unknown> = { ...defaults };
|
|
24
|
+
for (const [key, value] of Object.entries(args)) {
|
|
25
|
+
const base = out[key];
|
|
26
|
+
out[key] = isPlainObject(base) && isPlainObject(value)
|
|
27
|
+
? mergeDefaults(base, value)
|
|
28
|
+
: value;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { EvidenceRecord } from "./evidence.js";
|
|
2
|
+
|
|
3
|
+
/** A single validation check result. */
|
|
4
|
+
export interface ValidationEntry {
|
|
5
|
+
message: string;
|
|
6
|
+
evidenceLabel?: string;
|
|
7
|
+
detail?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Result of running all deterministic validation checks. */
|
|
11
|
+
export interface ValidationResult {
|
|
12
|
+
passes: ValidationEntry[];
|
|
13
|
+
failures: ValidationEntry[];
|
|
14
|
+
warnings: ValidationEntry[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Create an empty validation result. */
|
|
18
|
+
export function emptyValidationResult(): ValidationResult {
|
|
19
|
+
return { passes: [], failures: [], warnings: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Check that market-sensitive evidence records have timestamps. */
|
|
23
|
+
export function checkTimestamps(
|
|
24
|
+
evidence: EvidenceRecord[],
|
|
25
|
+
marketSensitiveLabels: Set<string>,
|
|
26
|
+
): ValidationEntry[] {
|
|
27
|
+
const warnings: ValidationEntry[] = [];
|
|
28
|
+
for (const record of evidence) {
|
|
29
|
+
if (
|
|
30
|
+
marketSensitiveLabels.has(record.label) &&
|
|
31
|
+
record.provenance.source === "fetched" &&
|
|
32
|
+
!record.provenance.timestamp
|
|
33
|
+
) {
|
|
34
|
+
warnings.push({
|
|
35
|
+
message: `Market-sensitive value '${record.label}' has no timestamp`,
|
|
36
|
+
evidenceLabel: record.label,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return warnings;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Check that options expiry dates are in the future. */
|
|
44
|
+
export function checkOptionsExpiries(
|
|
45
|
+
evidence: EvidenceRecord[],
|
|
46
|
+
today: string,
|
|
47
|
+
): ValidationEntry[] {
|
|
48
|
+
const failures: ValidationEntry[] = [];
|
|
49
|
+
for (const record of evidence) {
|
|
50
|
+
if (
|
|
51
|
+
record.label.toLowerCase().includes("expir") &&
|
|
52
|
+
typeof record.value === "string" &&
|
|
53
|
+
record.value < today
|
|
54
|
+
) {
|
|
55
|
+
failures.push({
|
|
56
|
+
message: `Options expiry ${record.value} is in the past`,
|
|
57
|
+
evidenceLabel: record.label,
|
|
58
|
+
detail: `Today is ${today}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return failures;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Check that all required fields have evidence records. */
|
|
66
|
+
export function checkRequiredFields(
|
|
67
|
+
evidence: EvidenceRecord[],
|
|
68
|
+
requiredLabels: string[],
|
|
69
|
+
): ValidationEntry[] {
|
|
70
|
+
const present = new Set(evidence.map((e) => e.label));
|
|
71
|
+
const failures: ValidationEntry[] = [];
|
|
72
|
+
for (const label of requiredLabels) {
|
|
73
|
+
if (!present.has(label)) {
|
|
74
|
+
failures.push({
|
|
75
|
+
message: `Required field '${label}' has no evidence record`,
|
|
76
|
+
evidenceLabel: label,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return failures;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Default market-sensitive labels. */
|
|
84
|
+
export const DEFAULT_MARKET_SENSITIVE_LABELS = new Set([
|
|
85
|
+
"Stock Price",
|
|
86
|
+
"Volume",
|
|
87
|
+
"Market Cap",
|
|
88
|
+
"52-Week High",
|
|
89
|
+
"52-Week Low",
|
|
90
|
+
"Bid",
|
|
91
|
+
"Ask",
|
|
92
|
+
"Day High",
|
|
93
|
+
"Day Low",
|
|
94
|
+
"Open",
|
|
95
|
+
"Previous Close",
|
|
96
|
+
"Crypto Price",
|
|
97
|
+
"Crypto Volume",
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
/** Configuration for the runtime validator. */
|
|
101
|
+
export interface ValidatorConfig {
|
|
102
|
+
marketSensitiveLabels?: Set<string>;
|
|
103
|
+
requiredFields?: string[];
|
|
104
|
+
toolResults?: Map<string, number>;
|
|
105
|
+
today?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Orchestrates all deterministic validation checks on evidence records.
|
|
110
|
+
* Runs before LLM-based validation.
|
|
111
|
+
*/
|
|
112
|
+
export class RuntimeValidator {
|
|
113
|
+
private readonly config: ValidatorConfig;
|
|
114
|
+
|
|
115
|
+
constructor(config: ValidatorConfig = {}) {
|
|
116
|
+
this.config = config;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Run all validation checks and return a combined result. */
|
|
120
|
+
validate(evidence: EvidenceRecord[]): ValidationResult {
|
|
121
|
+
const result = emptyValidationResult();
|
|
122
|
+
|
|
123
|
+
// Timestamp checks
|
|
124
|
+
const timestampWarnings = checkTimestamps(
|
|
125
|
+
evidence,
|
|
126
|
+
this.config.marketSensitiveLabels ?? DEFAULT_MARKET_SENSITIVE_LABELS,
|
|
127
|
+
);
|
|
128
|
+
result.warnings.push(...timestampWarnings);
|
|
129
|
+
|
|
130
|
+
// Options expiry checks
|
|
131
|
+
const today = this.config.today ?? new Date().toISOString().slice(0, 10);
|
|
132
|
+
const expiryFailures = checkOptionsExpiries(evidence, today);
|
|
133
|
+
result.failures.push(...expiryFailures);
|
|
134
|
+
|
|
135
|
+
// Required field checks
|
|
136
|
+
if (this.config.requiredFields) {
|
|
137
|
+
const fieldFailures = checkRequiredFields(evidence, this.config.requiredFields);
|
|
138
|
+
result.failures.push(...fieldFailures);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Number match checks
|
|
142
|
+
if (this.config.toolResults) {
|
|
143
|
+
const numberEntries = checkNumberMatch(evidence, this.config.toolResults);
|
|
144
|
+
for (const entry of numberEntries) {
|
|
145
|
+
if ((entry as any).type === "pass") {
|
|
146
|
+
result.passes.push(entry);
|
|
147
|
+
} else {
|
|
148
|
+
result.failures.push(entry);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Format validation results as a summary string for the LLM validation prompt. */
|
|
157
|
+
formatForLLM(result: ValidationResult): string {
|
|
158
|
+
const lines: string[] = ["## Deterministic Validation Results"];
|
|
159
|
+
|
|
160
|
+
if (result.failures.length > 0) {
|
|
161
|
+
lines.push(`\n### Failures (${result.failures.length})`);
|
|
162
|
+
for (const f of result.failures) {
|
|
163
|
+
lines.push(`- ${f.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (result.warnings.length > 0) {
|
|
168
|
+
lines.push(`\n### Warnings (${result.warnings.length})`);
|
|
169
|
+
for (const w of result.warnings) {
|
|
170
|
+
lines.push(`- ${w.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (result.passes.length > 0) {
|
|
175
|
+
lines.push(`\n### Verified (${result.passes.length})`);
|
|
176
|
+
for (const p of result.passes) {
|
|
177
|
+
lines.push(`- ${p.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (result.failures.length === 0 && result.warnings.length === 0) {
|
|
182
|
+
lines.push("\nAll deterministic checks passed.");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return lines.join("\n");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Check that evidence values match expected tool result values. */
|
|
190
|
+
export function checkNumberMatch(
|
|
191
|
+
evidence: EvidenceRecord[],
|
|
192
|
+
toolResults: Map<string, number>,
|
|
193
|
+
): ValidationEntry[] {
|
|
194
|
+
const results: (ValidationEntry & { type: "pass" | "failure" })[] = [];
|
|
195
|
+
for (const record of evidence) {
|
|
196
|
+
if (typeof record.value !== "number") continue;
|
|
197
|
+
const expected = toolResults.get(record.label);
|
|
198
|
+
if (expected === undefined) continue;
|
|
199
|
+
if (record.value === expected) {
|
|
200
|
+
results.push({
|
|
201
|
+
type: "pass",
|
|
202
|
+
message: `${record.label}: ${record.value} matches tool result`,
|
|
203
|
+
evidenceLabel: record.label,
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
results.push({
|
|
207
|
+
type: "failure",
|
|
208
|
+
message: `${record.label} mismatch: evidence says ${record.value}, tool returned ${expected}`,
|
|
209
|
+
evidenceLabel: record.label,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return results;
|
|
214
|
+
}
|