opencandle 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/dist/analysts/orchestrator.d.ts +3 -4
- package/dist/analysts/orchestrator.js +34 -44
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli-main.js +242 -9
- package/dist/cli-main.js.map +1 -1
- package/dist/config.d.ts +4 -9
- package/dist/config.js +7 -10
- package/dist/config.js.map +1 -1
- package/dist/doctor/report.js +1 -1
- package/dist/doctor/report.js.map +1 -1
- package/dist/infra/cache.d.ts +6 -0
- package/dist/infra/cache.js +16 -6
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/freshness.d.ts +21 -0
- package/dist/infra/freshness.js +119 -0
- package/dist/infra/freshness.js.map +1 -0
- package/dist/infra/market-calendar.d.ts +14 -0
- package/dist/infra/market-calendar.js +137 -0
- package/dist/infra/market-calendar.js.map +1 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/daily-report.d.ts +1 -1
- package/dist/market-state/daily-report.js +5 -16
- package/dist/market-state/daily-report.js.map +1 -1
- package/dist/market-state/service.d.ts +0 -35
- package/dist/market-state/service.js +0 -63
- package/dist/market-state/service.js.map +1 -1
- package/dist/market-state/summaries.d.ts +8 -0
- package/dist/market-state/summaries.js +70 -0
- package/dist/market-state/summaries.js.map +1 -0
- package/dist/memory/sqlite.js +20 -19
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/onboarding/providers.d.ts +25 -1
- package/dist/onboarding/providers.js +26 -0
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/pi/opencandle-extension.js +57 -226
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-action-dedupe.d.ts +6 -0
- package/dist/pi/session-action-dedupe.js +126 -0
- package/dist/pi/session-action-dedupe.js.map +1 -0
- package/dist/pi/session-writer-lock.d.ts +26 -2
- package/dist/pi/session-writer-lock.js +230 -18
- package/dist/pi/session-writer-lock.js.map +1 -1
- package/dist/pi/setup.js +5 -5
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tui-session-coordinator.d.ts +15 -0
- package/dist/pi/tui-session-coordinator.js +283 -0
- package/dist/pi/tui-session-coordinator.js.map +1 -0
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +1 -1
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/providers/alpha-vantage.js +6 -0
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -0
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/polymarket.d.ts +2 -0
- package/dist/providers/polymarket.js +163 -0
- package/dist/providers/polymarket.js.map +1 -0
- package/dist/providers/wrap-provider.js +5 -3
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -0
- package/dist/providers/yahoo-finance.js +206 -10
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +4 -5
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/entity-extractor.js +16 -0
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/route-manifest.js +1 -2
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-llm-client.js +18 -3
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router-prompt.js +1 -1
- package/dist/routing/router.d.ts +1 -1
- package/dist/routing/router.js +309 -9
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -0
- package/dist/routing/slot-resolver.js +1 -1
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/numeric-claims.d.ts +23 -0
- package/dist/runtime/numeric-claims.js +100 -0
- package/dist/runtime/numeric-claims.js.map +1 -0
- package/dist/runtime/planning-evidence.js +1 -76
- package/dist/runtime/planning-evidence.js.map +1 -1
- package/dist/runtime/prompt-step.d.ts +10 -1
- package/dist/runtime/prompt-step.js +116 -2
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +11 -1
- package/dist/runtime/session-coordinator.js +354 -62
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/validation.d.ts +3 -1
- package/dist/runtime/workflow-events.d.ts +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-types.d.ts +3 -0
- package/dist/runtime/workflow-types.js.map +1 -1
- package/dist/system-prompt.js +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -0
- package/dist/tools/fundamentals/comps.js +20 -9
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +190 -76
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/macro/event-probabilities.d.ts +8 -0
- package/dist/tools/macro/event-probabilities.js +121 -0
- package/dist/tools/macro/event-probabilities.js.map +1 -0
- package/dist/tools/market/crypto-price.d.ts +4 -1
- package/dist/tools/market/crypto-price.js +10 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.js +14 -3
- package/dist/tools/market/screen-stocks.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +4 -1
- package/dist/tools/market/stock-quote.js +9 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +4 -1
- package/dist/tools/options/option-chain.js +12 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/daily-report.js +10 -4
- package/dist/tools/portfolio/daily-report.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +23 -5
- package/dist/tools/technical/backtest.js +131 -94
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +1 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/fundamentals.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/market.d.ts +2 -0
- package/dist/types/options.d.ts +1 -0
- package/dist/types/prediction-markets.d.ts +13 -0
- package/dist/types/prediction-markets.js +2 -0
- package/dist/types/prediction-markets.js.map +1 -0
- package/gui/server/chat-event-adapter.ts +89 -5
- package/gui/server/http-routes.ts +661 -37
- package/gui/server/invoke-tool.ts +165 -16
- package/gui/server/live-chat-event-adapter.ts +37 -2
- package/gui/server/local-session-coordinator.ts +97 -0
- package/gui/server/market-state-api.ts +64 -48
- package/gui/server/projector.ts +132 -3
- package/gui/server/server.ts +51 -6
- package/gui/server/session-actions.ts +146 -45
- package/gui/server/session-entry-wait.ts +22 -3
- package/gui/server/tool-metadata.ts +3 -1
- package/gui/server/ws-hub.ts +77 -10
- package/gui/shared/chat-events.ts +10 -1
- package/gui/shared/event-reducer.ts +1 -0
- package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
- package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
- package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +13 -15
- package/src/analysts/orchestrator.ts +35 -56
- package/src/cli-main.ts +253 -13
- package/src/config.ts +12 -20
- package/src/doctor/report.ts +1 -1
- package/src/infra/cache.ts +24 -11
- package/src/infra/freshness.ts +165 -0
- package/src/infra/market-calendar.ts +193 -0
- package/src/infra/rate-limiter.ts +1 -0
- package/src/market-state/daily-report.ts +6 -16
- package/src/market-state/service.ts +0 -136
- package/src/market-state/summaries.ts +75 -0
- package/src/memory/sqlite.ts +23 -19
- package/src/onboarding/providers.ts +29 -1
- package/src/pi/opencandle-extension.ts +67 -270
- package/src/pi/session-action-dedupe.ts +155 -0
- package/src/pi/session-writer-lock.ts +290 -20
- package/src/pi/setup.ts +6 -6
- package/src/pi/tui-session-coordinator.ts +351 -0
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +1 -1
- package/src/providers/alpha-vantage.ts +6 -0
- package/src/providers/coingecko.ts +2 -0
- package/src/providers/polymarket.ts +214 -0
- package/src/providers/wrap-provider.ts +5 -3
- package/src/providers/yahoo-finance.ts +339 -27
- package/src/routing/classify-intent.ts +4 -5
- package/src/routing/entity-extractor.ts +17 -0
- package/src/routing/route-manifest.ts +1 -2
- package/src/routing/router-llm-client.ts +31 -19
- package/src/routing/router-prompt.ts +1 -1
- package/src/routing/router.ts +379 -8
- package/src/routing/slot-resolver.ts +1 -1
- package/src/runtime/evidence.ts +1 -0
- package/src/runtime/numeric-claims.ts +108 -0
- package/src/runtime/planning-evidence.ts +6 -91
- package/src/runtime/prompt-step.ts +127 -2
- package/src/runtime/session-coordinator.ts +464 -74
- package/src/runtime/validation.ts +1 -1
- package/src/runtime/workflow-events.ts +2 -0
- package/src/runtime/workflow-types.ts +3 -0
- package/src/system-prompt.ts +1 -1
- package/src/tools/AGENTS.md +1 -1
- package/src/tools/fundamentals/comps.ts +25 -14
- package/src/tools/fundamentals/dcf.ts +217 -87
- package/src/tools/index.ts +3 -3
- package/src/tools/macro/event-probabilities.ts +141 -0
- package/src/tools/market/crypto-price.ts +14 -2
- package/src/tools/market/screen-stocks.ts +14 -3
- package/src/tools/market/stock-quote.ts +49 -43
- package/src/tools/options/option-chain.ts +16 -2
- package/src/tools/portfolio/daily-report.ts +10 -4
- package/src/tools/portfolio/risk-analysis.ts +1 -1
- package/src/tools/technical/backtest.ts +167 -108
- package/src/tools/technical/indicators.ts +1 -1
- package/src/types/fundamentals.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/market.ts +2 -0
- package/src/types/options.ts +1 -0
- package/src/types/prediction-markets.ts +13 -0
- package/dist/tools/portfolio/predictions.d.ts +0 -55
- package/dist/tools/portfolio/predictions.js +0 -422
- package/dist/tools/portfolio/predictions.js.map +0 -1
- package/gui/server/chat-run-session.ts +0 -16
- package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
- package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
- package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
- package/src/tools/portfolio/predictions.ts +0 -553
|
@@ -4,14 +4,24 @@ import type { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import type { TSchema } from "@sinclair/typebox";
|
|
5
5
|
import { Value } from "@sinclair/typebox/value";
|
|
6
6
|
import { getDefaults } from "../../src/memory/tool-defaults.js";
|
|
7
|
+
import {
|
|
8
|
+
clearPendingSessionAction,
|
|
9
|
+
hasAcceptedSessionAction,
|
|
10
|
+
hasPendingSessionAction,
|
|
11
|
+
recordAcceptedSessionAction,
|
|
12
|
+
recordPendingSessionAction,
|
|
13
|
+
} from "../../src/pi/session-action-dedupe.js";
|
|
7
14
|
import { wrapWithDefaults } from "../../src/runtime/tool-defaults-wrapper.js";
|
|
8
15
|
import { getAllTools } from "../../src/tools/index.js";
|
|
9
16
|
import type { AskUserHandler } from "../../src/types/index.js";
|
|
17
|
+
import type { LocalSessionCoordinator } from "./local-session-coordinator.js";
|
|
10
18
|
import { buildToolInvokeAckMessage } from "./tool-invoke-ack.js";
|
|
11
19
|
import {
|
|
12
20
|
acquireWriterLock,
|
|
21
|
+
readWriterLock,
|
|
13
22
|
refreshWriterLock,
|
|
14
23
|
releaseWriterLock,
|
|
24
|
+
shouldBlockFailedCoordinatorAction,
|
|
15
25
|
writerLockScopeForSession,
|
|
16
26
|
} from "./writer-lock.js";
|
|
17
27
|
|
|
@@ -30,6 +40,7 @@ export interface ToolInvokeController {
|
|
|
30
40
|
toolName: string,
|
|
31
41
|
args: Record<string, unknown>,
|
|
32
42
|
sessionId?: string,
|
|
43
|
+
options?: { actionId?: string; allowProxy?: boolean },
|
|
33
44
|
): Promise<InvokeToolResult>;
|
|
34
45
|
handleToolInvokeMessage(client: ToolInvokeClient, data: Record<string, unknown>): Promise<void>;
|
|
35
46
|
}
|
|
@@ -46,6 +57,10 @@ export interface ToolInvokeControllerOptions {
|
|
|
46
57
|
resolveSessionManager?: (sessionId: string) => Promise<SessionManager | null>;
|
|
47
58
|
broadcastSessionSnapshot?: (sessionManager: SessionManager) => void;
|
|
48
59
|
broadcastSessions?: () => void;
|
|
60
|
+
localSessionCoordinator?: LocalSessionCoordinator;
|
|
61
|
+
localCoordinatorEndpoint?: string;
|
|
62
|
+
localCoordinatorSecret?: string;
|
|
63
|
+
syncWriterLockScope?: () => void;
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
export function createToolInvokeController({
|
|
@@ -60,13 +75,17 @@ export function createToolInvokeController({
|
|
|
60
75
|
resolveSessionManager,
|
|
61
76
|
broadcastSessionSnapshot,
|
|
62
77
|
broadcastSessions,
|
|
78
|
+
localSessionCoordinator,
|
|
79
|
+
localCoordinatorEndpoint,
|
|
80
|
+
localCoordinatorSecret,
|
|
81
|
+
syncWriterLockScope,
|
|
63
82
|
}: ToolInvokeControllerOptions): ToolInvokeController {
|
|
64
83
|
async function handleToolInvoke(
|
|
65
84
|
toolName: string,
|
|
66
85
|
args: Record<string, unknown>,
|
|
67
86
|
sessionId = "",
|
|
87
|
+
options: { actionId?: string; allowProxy?: boolean } = {},
|
|
68
88
|
): Promise<InvokeToolResult> {
|
|
69
|
-
if (role !== "writer") throw new Error("Read-only follower mode");
|
|
70
89
|
const tool = getTools().find((candidate) => candidate.name === toolName);
|
|
71
90
|
if (!tool) throw new Error(`Unknown tool: ${toolName}`);
|
|
72
91
|
|
|
@@ -77,15 +96,50 @@ export function createToolInvokeController({
|
|
|
77
96
|
resolveSessionManager,
|
|
78
97
|
);
|
|
79
98
|
const useCurrentSession = sameSessionStorage(currentSessionManager, runSessionManager);
|
|
99
|
+
const allowProxy = options.allowProxy !== false;
|
|
100
|
+
const shouldProxyToolInvoke =
|
|
101
|
+
allowProxy &&
|
|
102
|
+
(role !== "writer" || !useCurrentSession) &&
|
|
103
|
+
canProxyToolInvokeToCoordinator(runSessionManager);
|
|
104
|
+
if (shouldProxyToolInvoke) {
|
|
105
|
+
const proxied = await proxyToolInvokeToCoordinator(
|
|
106
|
+
runSessionManager,
|
|
107
|
+
toolName,
|
|
108
|
+
args,
|
|
109
|
+
options.actionId,
|
|
110
|
+
);
|
|
111
|
+
if (proxied) return proxied;
|
|
112
|
+
if (options.actionId && hasAcceptedSessionAction(runSessionManager, options.actionId)) {
|
|
113
|
+
throw new Error("OpenCandle already accepted this action in the active session.");
|
|
114
|
+
}
|
|
115
|
+
if (options.actionId && hasPendingSessionAction(runSessionManager, options.actionId)) {
|
|
116
|
+
throw new Error("OpenCandle is reconnecting to this session.");
|
|
117
|
+
}
|
|
118
|
+
if (options.actionId && shouldBlockFailedCoordinatorAction(runSessionManager)) {
|
|
119
|
+
throw new Error("OpenCandle is reconnecting to this session.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (options.actionId && hasAcceptedSessionAction(runSessionManager, options.actionId)) {
|
|
123
|
+
throw new Error("OpenCandle already accepted this action in the active session.");
|
|
124
|
+
}
|
|
125
|
+
if (options.actionId && hasPendingSessionAction(runSessionManager, options.actionId)) {
|
|
126
|
+
throw new Error("OpenCandle is reconnecting to this session.");
|
|
127
|
+
}
|
|
80
128
|
let acquiredLockScope = "";
|
|
81
129
|
let lockHeartbeat: ReturnType<typeof setInterval> | undefined;
|
|
82
|
-
|
|
130
|
+
const needsWriterLock = !useCurrentSession || role !== "writer";
|
|
131
|
+
// The writer fast path must fail closed like chat runs when this process
|
|
132
|
+
// lost its lock scope migration, or it can double-write to a session
|
|
133
|
+
// another process now owns.
|
|
134
|
+
if (!needsWriterLock) syncWriterLockScope?.();
|
|
135
|
+
if (needsWriterLock) {
|
|
83
136
|
const lockScope = writerLockScopeForSession(runSessionManager);
|
|
84
|
-
const lockResult = await acquireWriterLock(lockScope, "gui"
|
|
137
|
+
const lockResult = await acquireWriterLock(lockScope, "gui", {
|
|
138
|
+
coordinatorEndpoint: localCoordinatorEndpoint,
|
|
139
|
+
coordinatorSecret: localCoordinatorSecret,
|
|
140
|
+
});
|
|
85
141
|
if (lockResult.role !== "writer") {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`Session is currently being written by ${lockResult.lock.processKind} (pid ${lockResult.lock.pid}).`,
|
|
88
|
-
);
|
|
142
|
+
throw new Error("OpenCandle is reconnecting to this session.");
|
|
89
143
|
}
|
|
90
144
|
acquiredLockScope = lockScope;
|
|
91
145
|
lockHeartbeat = setInterval(() => refreshWriterLock(lockScope), 5000);
|
|
@@ -93,12 +147,27 @@ export function createToolInvokeController({
|
|
|
93
147
|
|
|
94
148
|
try {
|
|
95
149
|
const runSessionId = safeSessionId(runSessionManager);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
150
|
+
recordPendingSessionAction(runSessionManager, options.actionId ?? "");
|
|
151
|
+
let actionAccepted = false;
|
|
152
|
+
const recordAcceptedAction = () => {
|
|
153
|
+
if (actionAccepted) return;
|
|
154
|
+
recordAcceptedSessionAction(runSessionManager, options.actionId ?? "");
|
|
155
|
+
actionAccepted = true;
|
|
156
|
+
};
|
|
157
|
+
let result: InvokeToolResult;
|
|
158
|
+
try {
|
|
159
|
+
result = await invokeTool(runSessionManager, tool, args, "ui", {
|
|
160
|
+
askUserHandler:
|
|
161
|
+
runSessionId && askUserHandlerForSessionId
|
|
162
|
+
? askUserHandlerForSessionId(runSessionId)
|
|
163
|
+
: askUserHandler,
|
|
164
|
+
onTranscriptStarted: recordAcceptedAction,
|
|
165
|
+
});
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (!actionAccepted) clearPendingSessionAction(runSessionManager, options.actionId ?? "");
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
recordAcceptedAction();
|
|
102
171
|
if (!result.isError && marketStateToolMapping(toolName) != null) {
|
|
103
172
|
onMarketStateChanged?.();
|
|
104
173
|
}
|
|
@@ -120,10 +189,29 @@ export function createToolInvokeController({
|
|
|
120
189
|
data: Record<string, unknown>,
|
|
121
190
|
): Promise<void> {
|
|
122
191
|
const requestId = typeof data.requestId === "string" ? data.requestId : "";
|
|
192
|
+
const actionId = typeof data.actionId === "string" ? data.actionId : "";
|
|
123
193
|
const toolName = String(data.toolName ?? "");
|
|
124
194
|
const sessionId = typeof data.sessionId === "string" ? data.sessionId : "";
|
|
195
|
+
const allowProxy = data.allowProxy !== false;
|
|
125
196
|
try {
|
|
126
|
-
|
|
197
|
+
if (!sessionId.trim()) throw new Error("sessionId is required");
|
|
198
|
+
if (!actionId.trim()) throw new Error("actionId is required");
|
|
199
|
+
const invoke = () =>
|
|
200
|
+
handleToolInvoke(toolName, requestArgs(data.args), sessionId, { actionId, allowProxy });
|
|
201
|
+
const actionResult = localSessionCoordinator
|
|
202
|
+
? await localSessionCoordinator.runSessionAction(
|
|
203
|
+
{
|
|
204
|
+
sessionId,
|
|
205
|
+
actionId,
|
|
206
|
+
actionType: "tool.invoke",
|
|
207
|
+
payload: { toolName, args: requestArgs(data.args) },
|
|
208
|
+
source: "browser",
|
|
209
|
+
},
|
|
210
|
+
invoke,
|
|
211
|
+
)
|
|
212
|
+
: await invoke().then((result) => ({ ok: true as const, duplicate: false, result }));
|
|
213
|
+
if (!actionResult.ok) throw new Error(actionResult.message);
|
|
214
|
+
const result = actionResult.result;
|
|
127
215
|
if (requestId) {
|
|
128
216
|
client.send(buildToolInvokeAckMessage(requestId, toolName, result));
|
|
129
217
|
}
|
|
@@ -146,6 +234,64 @@ export function createToolInvokeController({
|
|
|
146
234
|
return { handleToolInvoke, handleToolInvokeMessage };
|
|
147
235
|
}
|
|
148
236
|
|
|
237
|
+
export function canProxyToolInvokeToCoordinator(runSessionManager: SessionManager): boolean {
|
|
238
|
+
let lockScope: string;
|
|
239
|
+
try {
|
|
240
|
+
lockScope = writerLockScopeForSession(runSessionManager);
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
const lock = readWriterLock(lockScope);
|
|
245
|
+
return Boolean(
|
|
246
|
+
lock?.processKind === "gui" &&
|
|
247
|
+
lock.coordinatorEndpoint &&
|
|
248
|
+
lock.coordinatorSecret &&
|
|
249
|
+
lock.pid !== process.pid,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function proxyToolInvokeToCoordinator(
|
|
254
|
+
runSessionManager: SessionManager,
|
|
255
|
+
toolName: string,
|
|
256
|
+
args: Record<string, unknown>,
|
|
257
|
+
actionId = "",
|
|
258
|
+
): Promise<InvokeToolResult | null> {
|
|
259
|
+
const lock = readWriterLock(writerLockScopeForSession(runSessionManager));
|
|
260
|
+
if (!lock?.coordinatorEndpoint || !lock.coordinatorSecret) return null;
|
|
261
|
+
const endpoint = new URL("/api/local-coordinator/tool-invoke", lock.coordinatorEndpoint);
|
|
262
|
+
let response: Response;
|
|
263
|
+
try {
|
|
264
|
+
response = await fetch(endpoint, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"content-type": "application/json",
|
|
268
|
+
"x-opencandle-coordinator-secret": lock.coordinatorSecret,
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
sessionId: safeSessionId(runSessionManager),
|
|
272
|
+
actionId,
|
|
273
|
+
toolName,
|
|
274
|
+
args,
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const body = asRecord(await response.json());
|
|
281
|
+
const ackResult = asRecord(body.result);
|
|
282
|
+
if (!response.ok || !ackResult.toolCallId) {
|
|
283
|
+
throw new Error(String(body.error ?? "OpenCandle is reconnecting to this session."));
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
toolCallId: String(ackResult.toolCallId),
|
|
287
|
+
result: {
|
|
288
|
+
content: Array.isArray(ackResult.content) ? ackResult.content : [],
|
|
289
|
+
details: ackResult.details,
|
|
290
|
+
},
|
|
291
|
+
isError: Boolean(ackResult.isError),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
149
295
|
function sameSessionStorage(current: SessionManager, target: SessionManager): boolean {
|
|
150
296
|
if (current === target) return true;
|
|
151
297
|
const currentFile = safeSessionFile(current);
|
|
@@ -192,7 +338,11 @@ export async function invokeToolFromUi(
|
|
|
192
338
|
tool: AgentTool<TSchema, unknown>,
|
|
193
339
|
args: Record<string, unknown>,
|
|
194
340
|
source: "ui" | "background" = "ui",
|
|
195
|
-
options: {
|
|
341
|
+
options: {
|
|
342
|
+
askUserHandler?: AskUserHandler;
|
|
343
|
+
recordTranscript?: boolean;
|
|
344
|
+
onTranscriptStarted?: () => void;
|
|
345
|
+
} = {},
|
|
196
346
|
): Promise<InvokeToolResult> {
|
|
197
347
|
if (!Value.Check(tool.parameters, args)) {
|
|
198
348
|
const errors = [...Value.Errors(tool.parameters, args)]
|
|
@@ -222,6 +372,7 @@ export async function invokeToolFromUi(
|
|
|
222
372
|
const recordTranscript = options.recordTranscript ?? true;
|
|
223
373
|
if (recordTranscript) {
|
|
224
374
|
sessionManager.appendMessage(assistant);
|
|
375
|
+
options.onTranscriptStarted?.();
|
|
225
376
|
}
|
|
226
377
|
|
|
227
378
|
const wrapped = wrapWithDefaults(tool, getDefaults(tool.name));
|
|
@@ -299,8 +450,6 @@ function marketStateToolMapping(toolName: string): { domain: string; targetType:
|
|
|
299
450
|
return { domain: "watchlist", targetType: "watchlist_item" };
|
|
300
451
|
case "track_portfolio":
|
|
301
452
|
return { domain: "portfolio", targetType: "portfolio_lot" };
|
|
302
|
-
case "track_prediction":
|
|
303
|
-
return { domain: "predictions", targetType: "prediction" };
|
|
304
453
|
case "manage_alerts":
|
|
305
454
|
return { domain: "alerts", targetType: "alert_rule" };
|
|
306
455
|
case "daily_watchlist_report":
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ChatEvent,
|
|
5
|
+
MessageAttachmentChip,
|
|
6
|
+
MessageContent,
|
|
7
|
+
ToolOutput,
|
|
8
|
+
} from "../shared/chat-events.js";
|
|
4
9
|
|
|
5
10
|
export interface LiveChatEventAdapterOptions {
|
|
6
11
|
runId: string;
|
|
@@ -12,6 +17,7 @@ export interface LiveChatEventAdapterOptions {
|
|
|
12
17
|
* message with an expanded prompt; the live view renders this instead.
|
|
13
18
|
*/
|
|
14
19
|
originalPrompt?: string;
|
|
20
|
+
originalAttachments?: MessageAttachmentChip[];
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export interface LiveChatEventAdapter {
|
|
@@ -77,7 +83,12 @@ export function createLiveChatEventAdapter(
|
|
|
77
83
|
type: "message.completed",
|
|
78
84
|
runId: options.runId,
|
|
79
85
|
messageId,
|
|
80
|
-
content:
|
|
86
|
+
content: userMessageContent(message.content, text),
|
|
87
|
+
...(userCount === 1 &&
|
|
88
|
+
options.originalAttachments &&
|
|
89
|
+
options.originalAttachments.length > 0
|
|
90
|
+
? { attachments: options.originalAttachments }
|
|
91
|
+
: {}),
|
|
81
92
|
});
|
|
82
93
|
return;
|
|
83
94
|
}
|
|
@@ -192,6 +203,30 @@ function messageText(content: unknown): string {
|
|
|
192
203
|
.join("");
|
|
193
204
|
}
|
|
194
205
|
|
|
206
|
+
function userMessageContent(content: unknown, text: string): MessageContent[] {
|
|
207
|
+
const parts: MessageContent[] = [{ type: "text", text }];
|
|
208
|
+
if (!Array.isArray(content)) return parts;
|
|
209
|
+
for (const part of content) {
|
|
210
|
+
const record = asRecord(part);
|
|
211
|
+
if (record.type !== "image") continue;
|
|
212
|
+
const alt = typeof record.alt === "string" ? record.alt : undefined;
|
|
213
|
+
if (typeof record.url === "string") {
|
|
214
|
+
parts.push({ type: "image", url: record.url, alt });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (typeof record.data === "string" && typeof record.mimeType === "string") {
|
|
218
|
+
parts.push({
|
|
219
|
+
type: "image",
|
|
220
|
+
url: `data:${record.mimeType};base64,${record.data}`,
|
|
221
|
+
data: record.data,
|
|
222
|
+
mimeType: record.mimeType,
|
|
223
|
+
alt,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return parts;
|
|
228
|
+
}
|
|
229
|
+
|
|
195
230
|
function toolOutput(result: unknown, isError: boolean): ToolOutput {
|
|
196
231
|
const record = asRecord(result);
|
|
197
232
|
return {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export type SessionActionType =
|
|
2
|
+
| "chat.prompt"
|
|
3
|
+
| "tool.invoke"
|
|
4
|
+
| "ask_user.answer"
|
|
5
|
+
| "ask_user.cancel"
|
|
6
|
+
| "run.cancel";
|
|
7
|
+
|
|
8
|
+
export type SessionActionSource = "gui" | "tui" | "browser";
|
|
9
|
+
|
|
10
|
+
export interface SessionActionEnvelope {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
actionId: string;
|
|
13
|
+
actionType: SessionActionType;
|
|
14
|
+
payload: Record<string, unknown>;
|
|
15
|
+
source: SessionActionSource;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type SessionActionResult<T = unknown> =
|
|
19
|
+
| { ok: true; duplicate: boolean; result: T }
|
|
20
|
+
| { ok: false; code: "session_busy"; message: string };
|
|
21
|
+
|
|
22
|
+
export interface LocalSessionCoordinator {
|
|
23
|
+
runSessionAction<T>(
|
|
24
|
+
action: SessionActionEnvelope,
|
|
25
|
+
handler: (action: SessionActionEnvelope) => Promise<T>,
|
|
26
|
+
): Promise<SessionActionResult<T>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LocalSessionCoordinatorOptions {
|
|
30
|
+
dedupeRetentionMs?: number;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AcceptedAction {
|
|
35
|
+
expiresAt: number;
|
|
36
|
+
result: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_DEDUPE_RETENTION_MS = 10 * 60 * 1000;
|
|
40
|
+
const BUSY_MESSAGE = "OpenCandle is still working in this session. Try again when it finishes.";
|
|
41
|
+
|
|
42
|
+
export function createLocalSessionCoordinator(
|
|
43
|
+
options: LocalSessionCoordinatorOptions = {},
|
|
44
|
+
): LocalSessionCoordinator {
|
|
45
|
+
const now = options.now ?? Date.now;
|
|
46
|
+
const dedupeRetentionMs = options.dedupeRetentionMs ?? DEFAULT_DEDUPE_RETENTION_MS;
|
|
47
|
+
const acceptedActions = new Map<string, AcceptedAction>();
|
|
48
|
+
const activeActions = new Map<string, Promise<unknown>>();
|
|
49
|
+
const activeRunSessions = new Set<string>();
|
|
50
|
+
|
|
51
|
+
async function runSessionAction<T>(
|
|
52
|
+
action: SessionActionEnvelope,
|
|
53
|
+
handler: (action: SessionActionEnvelope) => Promise<T>,
|
|
54
|
+
): Promise<SessionActionResult<T>> {
|
|
55
|
+
pruneExpiredActions();
|
|
56
|
+
const actionKey = `${action.sessionId}:${action.actionId}`;
|
|
57
|
+
const accepted = acceptedActions.get(actionKey);
|
|
58
|
+
if (accepted) return { ok: true, duplicate: true, result: accepted.result as T };
|
|
59
|
+
const active = activeActions.get(actionKey);
|
|
60
|
+
if (active) return { ok: true, duplicate: true, result: (await active) as T };
|
|
61
|
+
|
|
62
|
+
if (isRunAdmissionAction(action) && activeRunSessions.has(action.sessionId)) {
|
|
63
|
+
return { ok: false, code: "session_busy", message: BUSY_MESSAGE };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isRunAdmissionAction(action)) activeRunSessions.add(action.sessionId);
|
|
67
|
+
const runAction = (async () => {
|
|
68
|
+
const result = await handler(action);
|
|
69
|
+
acceptedActions.set(actionKey, {
|
|
70
|
+
expiresAt: now() + dedupeRetentionMs,
|
|
71
|
+
result,
|
|
72
|
+
});
|
|
73
|
+
return result;
|
|
74
|
+
})();
|
|
75
|
+
activeActions.set(actionKey, runAction);
|
|
76
|
+
try {
|
|
77
|
+
const result = await runAction;
|
|
78
|
+
return { ok: true, duplicate: false, result };
|
|
79
|
+
} finally {
|
|
80
|
+
activeActions.delete(actionKey);
|
|
81
|
+
if (isRunAdmissionAction(action)) activeRunSessions.delete(action.sessionId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pruneExpiredActions(): void {
|
|
86
|
+
const currentTime = now();
|
|
87
|
+
for (const [actionKey, accepted] of acceptedActions) {
|
|
88
|
+
if (accepted.expiresAt <= currentTime) acceptedActions.delete(actionKey);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { runSessionAction };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isRunAdmissionAction(action: SessionActionEnvelope): boolean {
|
|
96
|
+
return action.actionType === "chat.prompt" || action.actionType === "tool.invoke";
|
|
97
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
|
+
import { buildFreshnessStamp } from "../../src/infra/freshness.js";
|
|
2
3
|
import { isZeroFilledQuote, searchYahooInstruments } from "../../src/market-state/resolve.js";
|
|
3
4
|
import { MarketStateService } from "../../src/market-state/service.js";
|
|
4
5
|
import { initDefaultDatabase } from "../../src/memory/sqlite.js";
|
|
@@ -9,7 +10,6 @@ export interface MarketStateSnapshot {
|
|
|
9
10
|
instruments: Array<NonNullable<ReturnType<MarketStateService["getInstrument"]>>>;
|
|
10
11
|
watchlist: ReturnType<MarketStateService["listWatchlistItems"]>;
|
|
11
12
|
portfolio: ReturnType<MarketStateService["listPortfolioLots"]>;
|
|
12
|
-
predictions: ReturnType<MarketStateService["listPredictions"]>;
|
|
13
13
|
alerts: ReturnType<MarketStateService["listAlertRules"]>;
|
|
14
14
|
alertEvents: ReturnType<MarketStateService["listAlertEvents"]>;
|
|
15
15
|
alertCheckRuns: ReturnType<MarketStateService["listAlertCheckRuns"]>;
|
|
@@ -38,12 +38,12 @@ export interface MarketStateQuoteSnapshot {
|
|
|
38
38
|
instrumentId: number;
|
|
39
39
|
symbol: string;
|
|
40
40
|
status: "ok" | "unavailable";
|
|
41
|
-
currentPrice?: number;
|
|
41
|
+
currentPrice?: number | null;
|
|
42
42
|
changePercent?: number;
|
|
43
|
-
marketValue?: number;
|
|
43
|
+
marketValue?: number | null;
|
|
44
44
|
totalCost: number;
|
|
45
|
-
pnl?: number;
|
|
46
|
-
pnlPercent?: number;
|
|
45
|
+
pnl?: number | null;
|
|
46
|
+
pnlPercent?: number | null;
|
|
47
47
|
allocationPercent?: number;
|
|
48
48
|
currency: string;
|
|
49
49
|
includedInTotals: boolean;
|
|
@@ -51,17 +51,6 @@ export interface MarketStateQuoteSnapshot {
|
|
|
51
51
|
stale?: boolean;
|
|
52
52
|
reason?: string;
|
|
53
53
|
}>;
|
|
54
|
-
predictionQuotes: Array<{
|
|
55
|
-
predictionId: number;
|
|
56
|
-
instrumentId: number;
|
|
57
|
-
symbol: string;
|
|
58
|
-
status: "ok" | "unavailable";
|
|
59
|
-
currentPrice?: number;
|
|
60
|
-
changePercent?: number;
|
|
61
|
-
fetchedAt?: string;
|
|
62
|
-
stale?: boolean;
|
|
63
|
-
reason?: string;
|
|
64
|
-
}>;
|
|
65
54
|
portfolioSummary: {
|
|
66
55
|
baseCurrency: string;
|
|
67
56
|
totalValue: number;
|
|
@@ -72,6 +61,30 @@ export interface MarketStateQuoteSnapshot {
|
|
|
72
61
|
};
|
|
73
62
|
}
|
|
74
63
|
|
|
64
|
+
interface SavedSymbolsMemoOptions {
|
|
65
|
+
ttlMs?: number;
|
|
66
|
+
now?: () => number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createSavedSymbolsMemo(
|
|
70
|
+
load: () => string[],
|
|
71
|
+
options: SavedSymbolsMemoOptions = {},
|
|
72
|
+
): () => string[] {
|
|
73
|
+
const ttlMs = options.ttlMs ?? 30_000;
|
|
74
|
+
const now = options.now ?? Date.now;
|
|
75
|
+
let cachedAt = 0;
|
|
76
|
+
let cached: string[] | null = null;
|
|
77
|
+
return () => {
|
|
78
|
+
const current = now();
|
|
79
|
+
if (cached && current - cachedAt < ttlMs) return cached;
|
|
80
|
+
cached = load();
|
|
81
|
+
cachedAt = current;
|
|
82
|
+
return cached;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const getSavedMarketStateSymbols = createSavedSymbolsMemo(loadSavedMarketStateSymbols);
|
|
87
|
+
|
|
75
88
|
export function buildMarketStateSnapshot(db?: Database.Database): MarketStateSnapshot {
|
|
76
89
|
const ownedDb = db ?? initDefaultDatabase();
|
|
77
90
|
const service = new MarketStateService(ownedDb);
|
|
@@ -86,7 +99,6 @@ export function buildMarketStateSnapshot(db?: Database.Database): MarketStateSna
|
|
|
86
99
|
.filter((instrument) => instrument != null),
|
|
87
100
|
watchlist: service.listWatchlistItems(),
|
|
88
101
|
portfolio: service.listPortfolioLots(),
|
|
89
|
-
predictions: service.listPredictions(),
|
|
90
102
|
alerts,
|
|
91
103
|
alertEvents: service.listAlertEvents(),
|
|
92
104
|
alertCheckRuns: service.listAlertCheckRuns(),
|
|
@@ -101,6 +113,30 @@ export function buildMarketStateSnapshot(db?: Database.Database): MarketStateSna
|
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
115
|
|
|
116
|
+
function loadSavedMarketStateSymbols(): string[] {
|
|
117
|
+
const db = initDefaultDatabase();
|
|
118
|
+
const service = new MarketStateService(db);
|
|
119
|
+
try {
|
|
120
|
+
const symbols = [
|
|
121
|
+
...service.listWatchlistItems().map((item) => item.symbol),
|
|
122
|
+
...service.listPortfolioLots().map((lot) => lot.symbol),
|
|
123
|
+
];
|
|
124
|
+
return normalizeSymbols(symbols);
|
|
125
|
+
} finally {
|
|
126
|
+
db.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeSymbols(symbols: string[]): string[] {
|
|
131
|
+
const normalized: string[] = [];
|
|
132
|
+
for (const symbol of symbols) {
|
|
133
|
+
const next = symbol.trim().toUpperCase();
|
|
134
|
+
if (!next || normalized.includes(next)) continue;
|
|
135
|
+
normalized.push(next);
|
|
136
|
+
}
|
|
137
|
+
return normalized;
|
|
138
|
+
}
|
|
139
|
+
|
|
104
140
|
export async function buildMarketStateQuoteSnapshot(
|
|
105
141
|
db?: Database.Database,
|
|
106
142
|
): Promise<MarketStateQuoteSnapshot> {
|
|
@@ -109,13 +145,8 @@ export async function buildMarketStateQuoteSnapshot(
|
|
|
109
145
|
try {
|
|
110
146
|
const watchlist = service.listWatchlistItems();
|
|
111
147
|
const portfolio = service.listPortfolioLots();
|
|
112
|
-
const predictions = service.listPredictions();
|
|
113
148
|
const symbols = [
|
|
114
|
-
...new Set([
|
|
115
|
-
...watchlist.map((item) => item.symbol),
|
|
116
|
-
...portfolio.map((lot) => lot.symbol),
|
|
117
|
-
...predictions.map((prediction) => prediction.symbol),
|
|
118
|
-
]),
|
|
149
|
+
...new Set([...watchlist.map((item) => item.symbol), ...portfolio.map((lot) => lot.symbol)]),
|
|
119
150
|
];
|
|
120
151
|
const quoteMap = new Map<string, Awaited<ReturnType<typeof fetchQuoteSnapshot>>>();
|
|
121
152
|
for (const symbol of symbols) {
|
|
@@ -224,34 +255,10 @@ export async function buildMarketStateQuoteSnapshot(
|
|
|
224
255
|
currency: quote.currency,
|
|
225
256
|
reason: quote.reason ?? "quote unavailable",
|
|
226
257
|
}));
|
|
227
|
-
const predictionQuotes = predictions.map((prediction) => {
|
|
228
|
-
const quote = quoteMap.get(prediction.symbol);
|
|
229
|
-
if (quote == null || quote.status === "unavailable") {
|
|
230
|
-
return {
|
|
231
|
-
predictionId: prediction.id,
|
|
232
|
-
instrumentId: prediction.instrumentId,
|
|
233
|
-
symbol: prediction.symbol,
|
|
234
|
-
status: "unavailable" as const,
|
|
235
|
-
reason: quote?.reason ?? "quote unavailable",
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
return {
|
|
239
|
-
predictionId: prediction.id,
|
|
240
|
-
instrumentId: prediction.instrumentId,
|
|
241
|
-
symbol: prediction.symbol,
|
|
242
|
-
status: "ok" as const,
|
|
243
|
-
currentPrice: quote.price,
|
|
244
|
-
changePercent: quote.changePercent,
|
|
245
|
-
fetchedAt: quote.fetchedAt,
|
|
246
|
-
stale: quote.stale,
|
|
247
|
-
};
|
|
248
|
-
});
|
|
249
|
-
|
|
250
258
|
return {
|
|
251
259
|
generatedAt,
|
|
252
260
|
watchlistQuotes,
|
|
253
261
|
portfolioQuotes: portfolioQuotesWithAllocation,
|
|
254
|
-
predictionQuotes,
|
|
255
262
|
portfolioSummary: {
|
|
256
263
|
baseCurrency,
|
|
257
264
|
totalValue,
|
|
@@ -304,11 +311,20 @@ async function fetchQuoteSnapshot(symbol: string): Promise<
|
|
|
304
311
|
if (isZeroFilledQuote(result.data)) {
|
|
305
312
|
return { status: "unavailable", reason: "Yahoo returned no valid market data." };
|
|
306
313
|
}
|
|
314
|
+
const freshness = buildFreshnessStamp({
|
|
315
|
+
asOf: result.data.asOf,
|
|
316
|
+
cached: result.cached,
|
|
317
|
+
stale: result.stale,
|
|
318
|
+
cachedAt: result.timestamp,
|
|
319
|
+
});
|
|
320
|
+
if (freshness.isStaleForSession) {
|
|
321
|
+
return { status: "unavailable", reason: "provider returned stale market data" };
|
|
322
|
+
}
|
|
307
323
|
return {
|
|
308
324
|
status: "ok",
|
|
309
325
|
price: result.data.price,
|
|
310
326
|
changePercent: result.data.changePercent,
|
|
311
|
-
fetchedAt: result.timestamp,
|
|
327
|
+
fetchedAt: freshness.providerDataAt ?? result.timestamp,
|
|
312
328
|
stale: result.stale,
|
|
313
329
|
currency: result.data.currency ?? null,
|
|
314
330
|
};
|