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
package/src/routing/router.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
legacyRouteForRouteKind,
|
|
13
13
|
routeKindFromLegacyRoute,
|
|
14
14
|
selectToolBundles,
|
|
15
|
+
workflowRequiredSlots,
|
|
15
16
|
} from "./route-manifest.js";
|
|
16
17
|
import { buildRouterPrompt } from "./router-prompt.js";
|
|
17
18
|
import type {
|
|
@@ -25,6 +26,7 @@ import type {
|
|
|
25
26
|
RouterSlot,
|
|
26
27
|
ToolBundleName,
|
|
27
28
|
} from "./router-types.js";
|
|
29
|
+
import { mapDteHintToTarget } from "./slot-resolver.js";
|
|
28
30
|
import { disambiguateSymbols } from "./symbol-disambiguator.js";
|
|
29
31
|
import type { ExtractedEntities, WorkflowType } from "./types.js";
|
|
30
32
|
|
|
@@ -56,7 +58,7 @@ export async function route(
|
|
|
56
58
|
let firstError: string | undefined;
|
|
57
59
|
try {
|
|
58
60
|
const raw = await client.complete(prompt);
|
|
59
|
-
return postProcessRouterOutput(input.text, validateRouterOutput(raw));
|
|
61
|
+
return postProcessRouterOutput(input.text, validateRouterOutput(raw), input);
|
|
60
62
|
} catch (err) {
|
|
61
63
|
firstError = err instanceof Error ? err.message : String(err);
|
|
62
64
|
}
|
|
@@ -65,10 +67,10 @@ export async function route(
|
|
|
65
67
|
try {
|
|
66
68
|
const retryPrompt = `${prompt}\n\n(Your previous response failed validation: ${firstError}. Return a valid JSON object conforming to RouterOutput. Nothing else.)`;
|
|
67
69
|
const raw = await client.complete(retryPrompt);
|
|
68
|
-
return postProcessRouterOutput(input.text, validateRouterOutput(raw));
|
|
70
|
+
return postProcessRouterOutput(input.text, validateRouterOutput(raw), input);
|
|
69
71
|
} catch {
|
|
70
72
|
// Persistent failure — return a minimal fallback with regex-extracted symbols.
|
|
71
|
-
return postProcessRouterOutput(input.text, minimalFallback(input.text));
|
|
73
|
+
return postProcessRouterOutput(input.text, minimalFallback(input.text), input);
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -127,7 +129,7 @@ export function validateRouterOutput(raw: string): RouterOutput {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
const entities = validateEntities(obj.entities);
|
|
130
|
-
const slots = validateSlots(obj.slots);
|
|
132
|
+
const slots = canonicalizeSymbolSlots(workflow, validateSlots(obj.slots));
|
|
131
133
|
const preference_updates = validatePreferenceUpdates(obj.preference_updates);
|
|
132
134
|
const missing_required = rawMissingRequired;
|
|
133
135
|
const tool_bundles = validateToolBundles(obj.tool_bundles);
|
|
@@ -191,15 +193,25 @@ function validateEntities(raw: unknown): ExtractedEntities {
|
|
|
191
193
|
return out;
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
export function postProcessRouterOutput(
|
|
196
|
+
export function postProcessRouterOutput(
|
|
197
|
+
text: string,
|
|
198
|
+
output: RouterOutput,
|
|
199
|
+
inputContext?: Pick<RouterInputContext, "priorTurns" | "profileSnapshot">,
|
|
200
|
+
): RouterOutput {
|
|
195
201
|
const extracted = extractEntities(text);
|
|
196
202
|
const deterministic = classifyWithLegacyRules(text);
|
|
197
203
|
let diagnostics: RouterDiagnostic[] = [...output.diagnostics];
|
|
204
|
+
const symbolsAfterAmbiguousFilter = output.entities.symbols.filter(
|
|
205
|
+
(symbol) => !isAmbiguousConceptUsage(text, symbol),
|
|
206
|
+
);
|
|
207
|
+
const droppedAmbiguousSymbols = output.entities.symbols.filter(
|
|
208
|
+
(symbol) => !symbolsAfterAmbiguousFilter.includes(symbol),
|
|
209
|
+
);
|
|
198
210
|
let next: RouterOutput = {
|
|
199
211
|
...output,
|
|
200
212
|
entities: {
|
|
201
213
|
...output.entities,
|
|
202
|
-
symbols:
|
|
214
|
+
symbols: symbolsAfterAmbiguousFilter,
|
|
203
215
|
budget: output.entities.budget ?? extracted.budget,
|
|
204
216
|
maxPremium: output.entities.maxPremium ?? extracted.maxPremium,
|
|
205
217
|
timeHorizon: output.entities.timeHorizon ?? extracted.timeHorizon,
|
|
@@ -219,6 +231,169 @@ export function postProcessRouterOutput(text: string, output: RouterOutput): Rou
|
|
|
219
231
|
diagnostics,
|
|
220
232
|
};
|
|
221
233
|
|
|
234
|
+
if (next.workflow === "compare_assets") {
|
|
235
|
+
const restorableExtractedSymbols = extracted.symbols.filter(
|
|
236
|
+
(symbol) => disambiguateSymbols([symbol], text).kept.length > 0,
|
|
237
|
+
);
|
|
238
|
+
const orderedSymbols = orderSymbolsByText(
|
|
239
|
+
text,
|
|
240
|
+
mergeSymbols(next.entities.symbols, restorableExtractedSymbols),
|
|
241
|
+
);
|
|
242
|
+
if (!sameStringArray(orderedSymbols, next.entities.symbols)) {
|
|
243
|
+
diagnostics.push({
|
|
244
|
+
code: "compare_symbols_canonicalized",
|
|
245
|
+
message: "compare symbols restored from deterministic extraction and ordered by user text",
|
|
246
|
+
});
|
|
247
|
+
next = {
|
|
248
|
+
...next,
|
|
249
|
+
entities: {
|
|
250
|
+
...next.entities,
|
|
251
|
+
symbols: orderedSymbols,
|
|
252
|
+
},
|
|
253
|
+
slots: syncSymbolListSlot(next.slots, orderedSymbols),
|
|
254
|
+
diagnostics,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Prior-turn symbols merged into the slot (by the model or by the sync
|
|
259
|
+
// above) must not claim user provenance: the user typed only what is in
|
|
260
|
+
// this turn's text, and the Assumptions block renders slot sources.
|
|
261
|
+
// preference/memory/default sources legitimately reference out-of-text
|
|
262
|
+
// symbols and are left alone.
|
|
263
|
+
if (symbolsSlotClaimsPriorTurnUserProvenance(next.slots, text, inputContext?.priorTurns)) {
|
|
264
|
+
diagnostics.push({
|
|
265
|
+
code: "symbols_slot_provenance_prior_context",
|
|
266
|
+
message:
|
|
267
|
+
"compare symbols slot includes symbols absent from the user text; source downgraded from user to prior_context",
|
|
268
|
+
});
|
|
269
|
+
next = {
|
|
270
|
+
...next,
|
|
271
|
+
slots: {
|
|
272
|
+
...next.slots,
|
|
273
|
+
symbols: {
|
|
274
|
+
...next.slots.symbols,
|
|
275
|
+
source: "prior_context",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
diagnostics,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// The DTE horizon is a named historical loss class; when the model drops
|
|
284
|
+
// the slot on an options dispatch, the deterministic extraction preserves
|
|
285
|
+
// the user's stated range (mirrors the profile risk-slot fill below).
|
|
286
|
+
const dteHint = next.entities.dteHint ?? extracted.dteHint;
|
|
287
|
+
if (next.workflow === "options_screener" && !next.slots.dte_target && dteHint) {
|
|
288
|
+
const dteTarget = mapDteHintToTarget(dteHint);
|
|
289
|
+
if (dteTarget) {
|
|
290
|
+
diagnostics.push({
|
|
291
|
+
code: "dte_slot_filled_from_extraction",
|
|
292
|
+
message: "dte_target slot filled from deterministic horizon extraction",
|
|
293
|
+
});
|
|
294
|
+
next = {
|
|
295
|
+
...next,
|
|
296
|
+
slots: {
|
|
297
|
+
...next.slots,
|
|
298
|
+
dte_target: {
|
|
299
|
+
value: dteTarget,
|
|
300
|
+
source: "user",
|
|
301
|
+
confidence: "high",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
diagnostics,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Model-vocabulary synonyms for the canonical asset_scope value
|
|
310
|
+
// ("etf_only" for "etf_focused") drift saved preferences and slot
|
|
311
|
+
// consumers onto private vocabularies; canonicalize before the echo
|
|
312
|
+
// suppression below so restatements still match the profile.
|
|
313
|
+
next = canonicalizeAssetScopeVocabulary(next);
|
|
314
|
+
|
|
315
|
+
// A preference update that restates the saved profile value is an echo,
|
|
316
|
+
// not a change; writing it pollutes preference provenance (the
|
|
317
|
+
// preference-ECHO contract, fixture 029).
|
|
318
|
+
const echoedUpdates = next.preference_updates.filter(
|
|
319
|
+
(update) => readProfileString(inputContext?.profileSnapshot, update.key) === update.value,
|
|
320
|
+
);
|
|
321
|
+
if (echoedUpdates.length > 0) {
|
|
322
|
+
diagnostics.push({
|
|
323
|
+
code: "preference_echo_suppressed",
|
|
324
|
+
message: `dropped preference update(s) restating the saved profile: ${echoedUpdates
|
|
325
|
+
.map((update) => update.key)
|
|
326
|
+
.join(", ")}`,
|
|
327
|
+
});
|
|
328
|
+
next = {
|
|
329
|
+
...next,
|
|
330
|
+
preference_updates: next.preference_updates.filter(
|
|
331
|
+
(update) => !echoedUpdates.includes(update),
|
|
332
|
+
),
|
|
333
|
+
diagnostics,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const profileRiskProfile = readProfileString(inputContext?.profileSnapshot, "risk_profile");
|
|
338
|
+
if (
|
|
339
|
+
next.workflow === "portfolio_builder" &&
|
|
340
|
+
profileRiskProfile &&
|
|
341
|
+
!next.slots.risk_profile &&
|
|
342
|
+
!next.entities.riskProfile
|
|
343
|
+
) {
|
|
344
|
+
diagnostics.push({
|
|
345
|
+
code: "profile_risk_slot_filled",
|
|
346
|
+
message: "risk_profile slot filled from investor profile snapshot",
|
|
347
|
+
});
|
|
348
|
+
next = {
|
|
349
|
+
...next,
|
|
350
|
+
slots: {
|
|
351
|
+
...next.slots,
|
|
352
|
+
risk_profile: {
|
|
353
|
+
value: profileRiskProfile,
|
|
354
|
+
source: "preference",
|
|
355
|
+
confidence: "high",
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
diagnostics,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
next.routeKind === "pass_through" &&
|
|
364
|
+
extracted.riskProfile &&
|
|
365
|
+
isConversationalRiskPreferenceUpdate(text, inputContext)
|
|
366
|
+
) {
|
|
367
|
+
diagnostics.push({
|
|
368
|
+
code: "conversational_risk_preference_recovered",
|
|
369
|
+
message: "risk preference update recovered from profile/prior-turn context",
|
|
370
|
+
});
|
|
371
|
+
next = {
|
|
372
|
+
...next,
|
|
373
|
+
routeKind: "agent_task",
|
|
374
|
+
route: "fallback",
|
|
375
|
+
entities: {
|
|
376
|
+
...next.entities,
|
|
377
|
+
riskProfile: extracted.riskProfile,
|
|
378
|
+
},
|
|
379
|
+
slots: {
|
|
380
|
+
...next.slots,
|
|
381
|
+
risk_profile: {
|
|
382
|
+
value: extracted.riskProfile,
|
|
383
|
+
source: "user",
|
|
384
|
+
confidence: "high",
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
preference_updates: ensurePreferenceUpdate(next.preference_updates, {
|
|
388
|
+
key: "risk_profile",
|
|
389
|
+
value: extracted.riskProfile,
|
|
390
|
+
confidence: "high",
|
|
391
|
+
source: "inferred",
|
|
392
|
+
}),
|
|
393
|
+
diagnostics,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
222
397
|
if (
|
|
223
398
|
next.workflow === "options_screener" &&
|
|
224
399
|
isExistingPositionOptionRequest(text, extracted) &&
|
|
@@ -543,6 +718,27 @@ export function postProcessRouterOutput(text: string, output: RouterOutput): Rou
|
|
|
543
718
|
};
|
|
544
719
|
}
|
|
545
720
|
|
|
721
|
+
if (
|
|
722
|
+
next.workflow === "compare_assets" &&
|
|
723
|
+
(disambiguated.dropped.length > 0 || droppedAmbiguousSymbols.length > 0) &&
|
|
724
|
+
next.entities.symbols.length < 2 &&
|
|
725
|
+
isExplicitMacroDataRequest(text)
|
|
726
|
+
) {
|
|
727
|
+
diagnostics.push({
|
|
728
|
+
code: "compare_route_corrected_to_macro_task",
|
|
729
|
+
message: "macro/source acronyms were not explicit tickers",
|
|
730
|
+
});
|
|
731
|
+
next = {
|
|
732
|
+
...next,
|
|
733
|
+
routeKind: "agent_task",
|
|
734
|
+
route: "fallback",
|
|
735
|
+
workflow: "general_finance_qa",
|
|
736
|
+
missing_required: [],
|
|
737
|
+
slots: removeSymbolSlots(next.slots),
|
|
738
|
+
diagnostics,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
546
742
|
const missingRequired = computeMissingRequiredSlots(
|
|
547
743
|
next.workflow,
|
|
548
744
|
next.entities,
|
|
@@ -659,7 +855,7 @@ function isStatefulTrackingRequest(text: string): boolean {
|
|
|
659
855
|
lower,
|
|
660
856
|
);
|
|
661
857
|
const hasStateObject =
|
|
662
|
-
/\b(?:watchlist|portfolio|holding|holdings|position|positions|
|
|
858
|
+
/\b(?:watchlist|portfolio|holding|holdings|position|positions|alert|alerts|daily\s+report|watchlist\s+report|report\s+history)\b/.test(
|
|
663
859
|
lower,
|
|
664
860
|
);
|
|
665
861
|
const hasPortfolioLotShape =
|
|
@@ -816,7 +1012,8 @@ function validateSlots(raw: unknown): Record<string, RouterSlot> {
|
|
|
816
1012
|
if (!VALID_CONFIDENCE.has(s.confidence as string)) {
|
|
817
1013
|
throw new Error(`slot ${key} has invalid confidence: ${JSON.stringify(s.confidence)}`);
|
|
818
1014
|
}
|
|
819
|
-
|
|
1015
|
+
const canonicalKey = canonicalSlotKey(key);
|
|
1016
|
+
out[canonicalKey] = {
|
|
820
1017
|
value: s.value,
|
|
821
1018
|
source: s.source as RouterSlot["source"],
|
|
822
1019
|
confidence: s.confidence as RouterSlot["confidence"],
|
|
@@ -825,6 +1022,180 @@ function validateSlots(raw: unknown): Record<string, RouterSlot> {
|
|
|
825
1022
|
return out;
|
|
826
1023
|
}
|
|
827
1024
|
|
|
1025
|
+
// The slot contract uses snake_case keys; non-Claude router models sometimes
|
|
1026
|
+
// emit camelCase (timeHorizon, riskProfile). Canonicalize deterministically so
|
|
1027
|
+
// downstream slot resolution and missing-required checks match.
|
|
1028
|
+
function canonicalSlotKey(key: string): string {
|
|
1029
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Models also drift between scalar `symbol` and one-element `symbols` slot
|
|
1033
|
+
// shapes. Canonicalize toward the workflow's manifest-declared required slot
|
|
1034
|
+
// name when the conversion is unambiguous.
|
|
1035
|
+
function canonicalizeSymbolSlots(
|
|
1036
|
+
workflow: RouterOutput["workflow"],
|
|
1037
|
+
slots: Record<string, RouterSlot>,
|
|
1038
|
+
): Record<string, RouterSlot> {
|
|
1039
|
+
if (!workflow) return slots;
|
|
1040
|
+
const required = workflowRequiredSlots(workflow);
|
|
1041
|
+
const next = { ...slots };
|
|
1042
|
+
if (required.includes("symbol") && next.symbol == null && next.symbols != null) {
|
|
1043
|
+
const value = next.symbols.value;
|
|
1044
|
+
if (Array.isArray(value) && value.length === 1) {
|
|
1045
|
+
next.symbol = { ...next.symbols, value: value[0] };
|
|
1046
|
+
delete next.symbols;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (required.includes("symbols") && next.symbols == null && next.symbol != null) {
|
|
1050
|
+
const value = next.symbol.value;
|
|
1051
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1052
|
+
next.symbols = { ...next.symbol, value: [value] };
|
|
1053
|
+
delete next.symbol;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return next;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function syncSymbolListSlot(
|
|
1060
|
+
slots: Record<string, RouterSlot>,
|
|
1061
|
+
symbols: string[],
|
|
1062
|
+
): Record<string, RouterSlot> {
|
|
1063
|
+
if (!slots.symbols || symbols.length === 0) return slots;
|
|
1064
|
+
return {
|
|
1065
|
+
...slots,
|
|
1066
|
+
symbols: {
|
|
1067
|
+
...slots.symbols,
|
|
1068
|
+
value: symbols,
|
|
1069
|
+
},
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const ASSET_SCOPE_SYNONYMS: Record<string, string> = {
|
|
1074
|
+
etf_only: "etf_focused",
|
|
1075
|
+
etfs_only: "etf_focused",
|
|
1076
|
+
only_etfs: "etf_focused",
|
|
1077
|
+
etf: "etf_focused",
|
|
1078
|
+
etfs: "etf_focused",
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
function canonicalAssetScope(value: unknown): string | undefined {
|
|
1082
|
+
if (typeof value !== "string") return undefined;
|
|
1083
|
+
return ASSET_SCOPE_SYNONYMS[value.toLowerCase()];
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function canonicalizeAssetScopeVocabulary(output: RouterOutput): RouterOutput {
|
|
1087
|
+
let next = output;
|
|
1088
|
+
const slotCanonical = canonicalAssetScope(next.slots.asset_scope?.value);
|
|
1089
|
+
if (slotCanonical) {
|
|
1090
|
+
next = {
|
|
1091
|
+
...next,
|
|
1092
|
+
slots: {
|
|
1093
|
+
...next.slots,
|
|
1094
|
+
asset_scope: { ...next.slots.asset_scope, value: slotCanonical },
|
|
1095
|
+
},
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
const entityCanonical = canonicalAssetScope(next.entities.assetScope);
|
|
1099
|
+
if (entityCanonical) {
|
|
1100
|
+
next = {
|
|
1101
|
+
...next,
|
|
1102
|
+
entities: { ...next.entities, assetScope: entityCanonical },
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
if (
|
|
1106
|
+
next.preference_updates.some(
|
|
1107
|
+
(update) => update.key === "asset_scope" && canonicalAssetScope(update.value),
|
|
1108
|
+
)
|
|
1109
|
+
) {
|
|
1110
|
+
next = {
|
|
1111
|
+
...next,
|
|
1112
|
+
preference_updates: next.preference_updates.map((update) =>
|
|
1113
|
+
update.key === "asset_scope"
|
|
1114
|
+
? { ...update, value: canonicalAssetScope(update.value) ?? update.value }
|
|
1115
|
+
: update,
|
|
1116
|
+
),
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
return next;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function symbolsSlotClaimsPriorTurnUserProvenance(
|
|
1123
|
+
slots: Record<string, RouterSlot>,
|
|
1124
|
+
text: string,
|
|
1125
|
+
priorTurns: RouterInputContext["priorTurns"] | undefined,
|
|
1126
|
+
): boolean {
|
|
1127
|
+
const slot = slots.symbols;
|
|
1128
|
+
if (!slot || slot.source !== "user" || !Array.isArray(slot.value)) return false;
|
|
1129
|
+
if (!priorTurns || priorTurns.length === 0) return false;
|
|
1130
|
+
const priorText = priorTurns.map((turn) => turn.text).join("\n");
|
|
1131
|
+
// A symbol absent from this turn's text but present in prior turns is a
|
|
1132
|
+
// carryover, not a user-typed value. Symbols absent from both (e.g. tickers
|
|
1133
|
+
// resolved from company names in the current text) are left alone.
|
|
1134
|
+
return slot.value.some(
|
|
1135
|
+
(symbol) =>
|
|
1136
|
+
typeof symbol === "string" &&
|
|
1137
|
+
symbolPosition(text, symbol) === Number.MAX_SAFE_INTEGER &&
|
|
1138
|
+
symbolPosition(priorText, symbol) !== Number.MAX_SAFE_INTEGER,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function removeSymbolSlots(slots: Record<string, RouterSlot>): Record<string, RouterSlot> {
|
|
1143
|
+
const next = { ...slots };
|
|
1144
|
+
delete next.symbol;
|
|
1145
|
+
delete next.symbols;
|
|
1146
|
+
return next;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function orderSymbolsByText(text: string, symbols: string[]): string[] {
|
|
1150
|
+
return [...symbols].sort((a, b) => symbolPosition(text, a) - symbolPosition(text, b));
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function symbolPosition(text: string, symbol: string): number {
|
|
1154
|
+
const escaped = escapeRegExp(symbol);
|
|
1155
|
+
const match = new RegExp(`\\$?${escaped}\\b`, "i").exec(text);
|
|
1156
|
+
return match?.index ?? Number.MAX_SAFE_INTEGER;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function sameStringArray(left: string[], right: string[]): boolean {
|
|
1160
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function readProfileString(
|
|
1164
|
+
profileSnapshot: RouterInputContext["profileSnapshot"] | undefined,
|
|
1165
|
+
key: string,
|
|
1166
|
+
): string | undefined {
|
|
1167
|
+
const value = profileSnapshot?.[key];
|
|
1168
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function isConversationalRiskPreferenceUpdate(
|
|
1172
|
+
text: string,
|
|
1173
|
+
inputContext: Pick<RouterInputContext, "priorTurns" | "profileSnapshot"> | undefined,
|
|
1174
|
+
): boolean {
|
|
1175
|
+
if (!/\b(?:now|lately|getting|becoming|thinking|feel|feeling|prefer|preference)\b/i.test(text)) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
// Finance context must come from the conversation itself — a saved
|
|
1179
|
+
// risk_profile alone must not convert non-finance chat ("more aggressive
|
|
1180
|
+
// on the tennis court lately") into a routed preference write.
|
|
1181
|
+
const priorText = inputContext?.priorTurns.map((turn) => turn.text).join(" ") ?? "";
|
|
1182
|
+
return /\b(?:profile|risk|portfolio|position|invest|sizing)\b/i.test(`${text} ${priorText}`);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function ensurePreferenceUpdate(
|
|
1186
|
+
updates: RouterPreferenceUpdate[],
|
|
1187
|
+
update: RouterPreferenceUpdate,
|
|
1188
|
+
): RouterPreferenceUpdate[] {
|
|
1189
|
+
if (updates.some((item) => item.key === update.key && item.value === update.value)) {
|
|
1190
|
+
return updates;
|
|
1191
|
+
}
|
|
1192
|
+
return [...updates, update];
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function escapeRegExp(value: string): string {
|
|
1196
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1197
|
+
}
|
|
1198
|
+
|
|
828
1199
|
function validatePreferenceUpdates(raw: unknown): RouterPreferenceUpdate[] {
|
|
829
1200
|
if (raw === undefined || raw === null) return [];
|
|
830
1201
|
if (!Array.isArray(raw)) {
|
|
@@ -19,7 +19,7 @@ interface Preferences {
|
|
|
19
19
|
liquidityMinimum?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function mapDteHintToTarget(dteHint: string | undefined): string | undefined {
|
|
22
|
+
export function mapDteHintToTarget(dteHint: string | undefined): string | undefined {
|
|
23
23
|
const normalized = dteHint?.toLowerCase().trim();
|
|
24
24
|
if (!normalized) return undefined;
|
|
25
25
|
|
package/src/runtime/evidence.ts
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { EvidenceRecord } from "./evidence.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Observe-only numeric-claim extraction for synthesis validation (I3).
|
|
5
|
+
*
|
|
6
|
+
* These heuristics bind numbers in analyst prose to tool-result metrics so
|
|
7
|
+
* checkNumberMatch has claims to verify. They are deliberately conservative:
|
|
8
|
+
* every false mismatch poisons the false-positive-rate data that gates the
|
|
9
|
+
* answer-receipts direction (I8 phase 1), so a missed claim is cheaper than
|
|
10
|
+
* a wrong one.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Flatten numeric values from a tool-evidence digest preview into labeled tool results. */
|
|
14
|
+
export function collectToolNumbers(record: EvidenceRecord, toolResults: Map<string, number>): void {
|
|
15
|
+
if (!isPlainObject(record.value)) return;
|
|
16
|
+
const tool = typeof record.value.tool === "string" ? record.value.tool : undefined;
|
|
17
|
+
const digest = isPlainObject(record.value.resultDigest) ? record.value.resultDigest : undefined;
|
|
18
|
+
const preview = typeof digest?.preview === "string" ? digest.preview : undefined;
|
|
19
|
+
if (!tool || !preview) return;
|
|
20
|
+
const parsed = parseMaybeJson(preview);
|
|
21
|
+
if (!parsed) return;
|
|
22
|
+
for (const [path, value] of flattenNumericValues(parsed)) {
|
|
23
|
+
toolResults.set(`${tool}.${path}`, value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract numeric claims from step text for metrics present in toolResults.
|
|
29
|
+
*
|
|
30
|
+
* Binding rules (each guards against a live-observed false-positive class):
|
|
31
|
+
* - the metric term must appear as a whole word ("opening" is not `open`)
|
|
32
|
+
* - the number must follow within a short window (no cross-sentence binding)
|
|
33
|
+
* - thousands separators parse as one number ("129,935,067" is not 129)
|
|
34
|
+
* - a prose value that rounds to the tool result is normalized onto it
|
|
35
|
+
* ("197.14" claims the tool's 197.13999938964844, not a mismatch)
|
|
36
|
+
*/
|
|
37
|
+
export function extractNumericClaims(
|
|
38
|
+
text: string,
|
|
39
|
+
toolResults: Map<string, number>,
|
|
40
|
+
): EvidenceRecord[] {
|
|
41
|
+
const records: EvidenceRecord[] = [];
|
|
42
|
+
for (const [label, expected] of toolResults) {
|
|
43
|
+
const metric = label.split(".").at(-1);
|
|
44
|
+
if (!metric) continue;
|
|
45
|
+
const pattern = new RegExp(
|
|
46
|
+
`\\b${escapeRegex(metric)}\\b[^\\d-]{0,30}(-?\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?|-?\\d+(?:\\.\\d+)?)`,
|
|
47
|
+
"i",
|
|
48
|
+
);
|
|
49
|
+
const match = text.match(pattern);
|
|
50
|
+
if (!match) continue;
|
|
51
|
+
const value = Number(match[1].replace(/,/g, ""));
|
|
52
|
+
if (!Number.isFinite(value)) continue;
|
|
53
|
+
records.push({
|
|
54
|
+
label,
|
|
55
|
+
value: isRoundingOf(value, expected) ? expected : value,
|
|
56
|
+
provenance: { source: "computed" },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return records;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True when `claim` is `expected` rounded to the claim's own precision. */
|
|
63
|
+
function isRoundingOf(claim: number, expected: number): boolean {
|
|
64
|
+
if (claim === expected) return true;
|
|
65
|
+
const decimals = countDecimals(claim);
|
|
66
|
+
const factor = 10 ** decimals;
|
|
67
|
+
return Math.round(expected * factor) / factor === claim;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function countDecimals(value: number): number {
|
|
71
|
+
const text = String(value);
|
|
72
|
+
const dot = text.indexOf(".");
|
|
73
|
+
return dot === -1 ? 0 : text.length - dot - 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function flattenNumericValues(
|
|
77
|
+
value: Record<string, unknown>,
|
|
78
|
+
prefix = "",
|
|
79
|
+
): Array<[string, number]> {
|
|
80
|
+
const values: Array<[string, number]> = [];
|
|
81
|
+
for (const [key, item] of Object.entries(value)) {
|
|
82
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
83
|
+
if (typeof item === "number" && Number.isFinite(item)) {
|
|
84
|
+
values.push([path, item]);
|
|
85
|
+
} else if (isPlainObject(item)) {
|
|
86
|
+
values.push(...flattenNumericValues(item, path));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return values;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function escapeRegex(value: string): string {
|
|
93
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseMaybeJson(raw: unknown): Record<string, unknown> | undefined {
|
|
97
|
+
if (typeof raw !== "string") return undefined;
|
|
98
|
+
try {
|
|
99
|
+
const parsed: unknown = JSON.parse(raw);
|
|
100
|
+
return isPlainObject(parsed) ? parsed : undefined;
|
|
101
|
+
} catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
107
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
108
|
+
}
|