opencandle 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/analysts/orchestrator.d.ts +2 -0
- package/dist/analysts/orchestrator.js +19 -0
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/doctor/report.js +1 -1
- package/dist/doctor/report.js.map +1 -1
- package/dist/infra/cache.d.ts +6 -0
- package/dist/infra/cache.js +16 -6
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/freshness.d.ts +21 -0
- package/dist/infra/freshness.js +119 -0
- package/dist/infra/freshness.js.map +1 -0
- package/dist/infra/market-calendar.d.ts +14 -0
- package/dist/infra/market-calendar.js +137 -0
- package/dist/infra/market-calendar.js.map +1 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/summaries.d.ts +8 -0
- package/dist/market-state/summaries.js +70 -0
- package/dist/market-state/summaries.js.map +1 -0
- package/dist/onboarding/providers.d.ts +25 -1
- package/dist/onboarding/providers.js +26 -0
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/pi/opencandle-extension.js +44 -5
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/providers/alpha-vantage.js +6 -0
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -0
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/polymarket.d.ts +2 -0
- package/dist/providers/polymarket.js +163 -0
- package/dist/providers/polymarket.js.map +1 -0
- package/dist/providers/wrap-provider.js +5 -3
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -0
- package/dist/providers/yahoo-finance.js +206 -10
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/entity-extractor.js +16 -0
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/route-manifest.js +1 -1
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-llm-client.js +18 -3
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router.d.ts +1 -1
- package/dist/routing/router.js +274 -5
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -0
- package/dist/routing/slot-resolver.js +1 -1
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/numeric-claims.d.ts +23 -0
- package/dist/runtime/numeric-claims.js +100 -0
- package/dist/runtime/numeric-claims.js.map +1 -0
- package/dist/runtime/planning-evidence.js +1 -76
- package/dist/runtime/planning-evidence.js.map +1 -1
- package/dist/runtime/prompt-step.d.ts +10 -1
- package/dist/runtime/prompt-step.js +116 -2
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +11 -1
- package/dist/runtime/session-coordinator.js +352 -49
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/validation.d.ts +3 -1
- package/dist/runtime/workflow-events.d.ts +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-types.d.ts +3 -0
- package/dist/runtime/workflow-types.js.map +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -0
- package/dist/tools/fundamentals/comps.js +20 -9
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.js +183 -112
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/macro/event-probabilities.d.ts +8 -0
- package/dist/tools/macro/event-probabilities.js +121 -0
- package/dist/tools/macro/event-probabilities.js.map +1 -0
- package/dist/tools/market/crypto-price.d.ts +4 -1
- package/dist/tools/market/crypto-price.js +10 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.js +14 -3
- package/dist/tools/market/screen-stocks.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +4 -1
- package/dist/tools/market/stock-quote.js +9 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +4 -1
- package/dist/tools/options/option-chain.js +12 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/technical/indicators.js +1 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/fundamentals.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/market.d.ts +2 -0
- package/dist/types/options.d.ts +1 -0
- package/dist/types/prediction-markets.d.ts +13 -0
- package/dist/types/prediction-markets.js +2 -0
- package/dist/types/prediction-markets.js.map +1 -0
- package/gui/server/chat-event-adapter.ts +89 -5
- package/gui/server/http-routes.ts +285 -37
- package/gui/server/invoke-tool.ts +14 -13
- package/gui/server/live-chat-event-adapter.ts +37 -2
- package/gui/server/market-state-api.ts +63 -5
- package/gui/server/projector.ts +132 -3
- package/gui/server/session-actions.ts +32 -40
- package/gui/server/session-entry-wait.ts +22 -3
- package/gui/server/ws-hub.ts +16 -4
- package/gui/shared/chat-events.ts +10 -1
- package/gui/shared/event-reducer.ts +1 -0
- package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
- package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
- package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +8 -13
- package/src/analysts/orchestrator.ts +20 -0
- package/src/doctor/report.ts +1 -1
- package/src/infra/cache.ts +24 -11
- package/src/infra/freshness.ts +165 -0
- package/src/infra/market-calendar.ts +193 -0
- package/src/infra/rate-limiter.ts +1 -0
- package/src/market-state/summaries.ts +75 -0
- package/src/onboarding/providers.ts +29 -1
- package/src/pi/opencandle-extension.ts +55 -5
- package/src/providers/alpha-vantage.ts +6 -0
- package/src/providers/coingecko.ts +2 -0
- package/src/providers/polymarket.ts +214 -0
- package/src/providers/wrap-provider.ts +5 -3
- package/src/providers/yahoo-finance.ts +339 -27
- package/src/routing/entity-extractor.ts +17 -0
- package/src/routing/route-manifest.ts +1 -1
- package/src/routing/router-llm-client.ts +31 -19
- package/src/routing/router.ts +340 -5
- package/src/routing/slot-resolver.ts +1 -1
- package/src/runtime/evidence.ts +1 -0
- package/src/runtime/numeric-claims.ts +108 -0
- package/src/runtime/planning-evidence.ts +6 -91
- package/src/runtime/prompt-step.ts +127 -2
- package/src/runtime/session-coordinator.ts +462 -57
- package/src/runtime/validation.ts +1 -1
- package/src/runtime/workflow-events.ts +2 -0
- package/src/runtime/workflow-types.ts +3 -0
- package/src/tools/fundamentals/comps.ts +25 -14
- package/src/tools/fundamentals/dcf.ts +204 -124
- package/src/tools/index.ts +3 -0
- package/src/tools/macro/event-probabilities.ts +141 -0
- package/src/tools/market/crypto-price.ts +14 -2
- package/src/tools/market/screen-stocks.ts +14 -3
- package/src/tools/market/stock-quote.ts +49 -43
- package/src/tools/options/option-chain.ts +16 -2
- package/src/tools/portfolio/risk-analysis.ts +1 -1
- package/src/tools/technical/indicators.ts +1 -1
- package/src/types/fundamentals.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/market.ts +2 -0
- package/src/types/options.ts +1 -0
- package/src/types/prediction-markets.ts +13 -0
- package/gui/server/chat-run-session.ts +0 -16
- package/gui/web/dist/assets/CatalogOverlay-ChRTKNlf.js +0 -1
- package/gui/web/dist/assets/index-BzyqyVnd.css +0 -2
- package/gui/web/dist/assets/index-DvMjkOP9.js +0 -65
|
@@ -3,6 +3,13 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
3
3
|
import { extname, join, resolve } from "node:path";
|
|
4
4
|
import { type AgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { buildDoctorReport } from "../../src/doctor/report.js";
|
|
6
|
+
import { MarketStateService } from "../../src/market-state/service.js";
|
|
7
|
+
import {
|
|
8
|
+
formatLatestReportSummary,
|
|
9
|
+
formatPortfolioSummary,
|
|
10
|
+
formatWatchlistSummary,
|
|
11
|
+
} from "../../src/market-state/summaries.js";
|
|
12
|
+
import { initDefaultDatabase } from "../../src/memory/sqlite.js";
|
|
6
13
|
import { probeProviderStatus } from "../../src/onboarding/provider-status.js";
|
|
7
14
|
import {
|
|
8
15
|
clearPendingSessionAction,
|
|
@@ -13,7 +20,6 @@ import {
|
|
|
13
20
|
} from "../../src/pi/session-action-dedupe.js";
|
|
14
21
|
import type { ChatEvent } from "../shared/chat-events.js";
|
|
15
22
|
import { sessionEntriesToChatEvents } from "./chat-event-adapter.js";
|
|
16
|
-
import { chatRunSessionConflict } from "./chat-run-session.js";
|
|
17
23
|
import type { ToolInvokeController } from "./invoke-tool.js";
|
|
18
24
|
import { createLiveChatEventAdapter } from "./live-chat-event-adapter.js";
|
|
19
25
|
import type {
|
|
@@ -21,7 +27,11 @@ import type {
|
|
|
21
27
|
SessionActionEnvelope,
|
|
22
28
|
SessionActionResult,
|
|
23
29
|
} from "./local-session-coordinator.js";
|
|
24
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
buildMarketStateSnapshot,
|
|
32
|
+
getSavedMarketStateSymbols,
|
|
33
|
+
searchInstrumentCandidates,
|
|
34
|
+
} from "./market-state-api.js";
|
|
25
35
|
import { buildModelSetupState, type ModelSetupController } from "./model-setup.js";
|
|
26
36
|
import { isTrustedPrivateApiRequest, privateApiCookieHeader } from "./private-api-access.js";
|
|
27
37
|
import { projectDashboard } from "./projector.js";
|
|
@@ -32,6 +42,7 @@ import { waitForNewEntryId } from "./session-entry-wait.js";
|
|
|
32
42
|
import { buildCatalog } from "./tool-metadata.js";
|
|
33
43
|
import {
|
|
34
44
|
acquireWriterLock,
|
|
45
|
+
isCoordinatorOwnerAlive,
|
|
35
46
|
readWriterLock,
|
|
36
47
|
refreshWriterLock,
|
|
37
48
|
releaseWriterLock,
|
|
@@ -64,6 +75,28 @@ interface GuiHttpRouteOptions {
|
|
|
64
75
|
localSessionCoordinator?: LocalSessionCoordinator;
|
|
65
76
|
}
|
|
66
77
|
|
|
78
|
+
export interface ChatRunImageInput {
|
|
79
|
+
data: string;
|
|
80
|
+
mimeType: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ChatRunAttachmentInput =
|
|
84
|
+
| { kind: "portfolio"; id?: string }
|
|
85
|
+
| { kind: "watchlist"; id: string }
|
|
86
|
+
| { kind: "report"; id: string };
|
|
87
|
+
|
|
88
|
+
export interface ParsedChatRunBody {
|
|
89
|
+
prompt: string;
|
|
90
|
+
images: ChatRunImageInput[];
|
|
91
|
+
attachments: ChatRunAttachmentInput[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type ChatRunParseResult = { ok: true; value: ParsedChatRunBody } | { ok: false; error: string };
|
|
95
|
+
|
|
96
|
+
const CHAT_RUN_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
97
|
+
const CHAT_RUN_MAX_IMAGE_COUNT = 4;
|
|
98
|
+
const CHAT_RUN_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
99
|
+
|
|
67
100
|
export function createHttpRequestHandler(options: GuiHttpRouteOptions) {
|
|
68
101
|
const activeRunSessionIds = new Set<string>();
|
|
69
102
|
|
|
@@ -234,28 +267,35 @@ export function createHttpRequestHandler(options: GuiHttpRouteOptions) {
|
|
|
234
267
|
|
|
235
268
|
if (url.pathname === "/api/chat/run" && req.method === "POST") {
|
|
236
269
|
if (!allowTrustedGuiRequest(req, res, "Chat run API", options)) return;
|
|
237
|
-
|
|
270
|
+
writeJson(
|
|
271
|
+
res,
|
|
272
|
+
{
|
|
273
|
+
error: "Legacy active-session chat runs are no longer supported.",
|
|
274
|
+
code: "legacy_route_removed",
|
|
275
|
+
},
|
|
276
|
+
410,
|
|
277
|
+
);
|
|
238
278
|
return;
|
|
239
279
|
}
|
|
240
280
|
|
|
241
281
|
if (url.pathname === "/api/local-coordinator/chat-run" && req.method === "POST") {
|
|
242
282
|
if (!allowLocalCoordinatorRequest(req, res, options)) return;
|
|
243
283
|
const body = asRecord(await readJsonBody(req));
|
|
244
|
-
|
|
245
|
-
const sessionManager =
|
|
246
|
-
|
|
247
|
-
: undefined;
|
|
248
|
-
if (requestedSessionId && !sessionManager) {
|
|
284
|
+
if (!requireSessionActionFields(res, body)) return;
|
|
285
|
+
const sessionManager = await resolveSessionManagerById(options, String(body.sessionId));
|
|
286
|
+
if (!sessionManager) {
|
|
249
287
|
writeJson(res, { error: "Unknown saved session" }, 404);
|
|
250
288
|
return;
|
|
251
289
|
}
|
|
252
|
-
|
|
290
|
+
// allowProxy: false — this endpoint is the proxy target; re-proxying loops.
|
|
291
|
+
await handleSseChatRun(req, res, options, activeRunSessionIds, sessionManager, body, false);
|
|
253
292
|
return;
|
|
254
293
|
}
|
|
255
294
|
|
|
256
295
|
if (url.pathname === "/api/local-coordinator/tool-invoke" && req.method === "POST") {
|
|
257
296
|
if (!allowLocalCoordinatorRequest(req, res, options)) return;
|
|
258
297
|
const body = asRecord(await readJsonBody(req));
|
|
298
|
+
if (!requireSessionActionFields(res, body)) return;
|
|
259
299
|
let ack: unknown;
|
|
260
300
|
await options.toolInvokeController.handleToolInvokeMessage(
|
|
261
301
|
{ send: (message) => (ack = message) },
|
|
@@ -285,6 +325,7 @@ export function createHttpRequestHandler(options: GuiHttpRouteOptions) {
|
|
|
285
325
|
if (url.pathname === "/api/local-coordinator/ask-user" && req.method === "POST") {
|
|
286
326
|
if (!allowLocalCoordinatorRequest(req, res, options)) return;
|
|
287
327
|
const body = asRecord(await readJsonBody(req));
|
|
328
|
+
if (!requireSessionActionFields(res, body)) return;
|
|
288
329
|
const payload = asRecord(body.payload);
|
|
289
330
|
try {
|
|
290
331
|
const action = {
|
|
@@ -319,12 +360,16 @@ export function createHttpRequestHandler(options: GuiHttpRouteOptions) {
|
|
|
319
360
|
const runSessionId = sessionIdFromRoute(url.pathname, "runs");
|
|
320
361
|
if (runSessionId && req.method === "POST") {
|
|
321
362
|
if (!allowTrustedGuiRequest(req, res, "Chat run API", options)) return;
|
|
363
|
+
const body = asRecord(await readJsonBody(req));
|
|
364
|
+
if (!requireSessionActionFields(res, { ...body, sessionId: runSessionId }, false)) return;
|
|
322
365
|
const sessionManager = await resolveSessionManagerById(options, runSessionId);
|
|
323
366
|
if (!sessionManager) {
|
|
324
367
|
writeJson(res, { error: "Unknown saved session" }, 404);
|
|
325
368
|
return;
|
|
326
369
|
}
|
|
327
|
-
|
|
370
|
+
// allowProxy: true — forward to a live coordinator owned by another
|
|
371
|
+
// process (0.11.0 authenticated local forwarding).
|
|
372
|
+
await handleSseChatRun(req, res, options, activeRunSessionIds, sessionManager, body, true);
|
|
328
373
|
return;
|
|
329
374
|
}
|
|
330
375
|
|
|
@@ -337,28 +382,26 @@ async function handleSseChatRun(
|
|
|
337
382
|
res: ServerResponse,
|
|
338
383
|
options: GuiHttpRouteOptions,
|
|
339
384
|
activeRunSessionIds: Set<string>,
|
|
340
|
-
targetSessionManager
|
|
341
|
-
bodyOverride
|
|
385
|
+
targetSessionManager: SessionManager | undefined,
|
|
386
|
+
bodyOverride: Record<string, unknown> | undefined,
|
|
387
|
+
// Explicit because both remaining callers pass a body: the browser-facing
|
|
388
|
+
// runs route must forward to a live coordinator owned by another process,
|
|
389
|
+
// while the local-coordinator endpoint IS the proxy target and re-proxying
|
|
390
|
+
// there would loop.
|
|
391
|
+
allowProxy: boolean,
|
|
342
392
|
): Promise<void> {
|
|
343
393
|
const body = bodyOverride ?? asRecord(await readJsonBody(req));
|
|
344
|
-
const
|
|
345
|
-
if (!
|
|
346
|
-
writeJson(res, { error:
|
|
394
|
+
const parsed = parseChatRunBody(body);
|
|
395
|
+
if (!parsed.ok) {
|
|
396
|
+
writeJson(res, { error: parsed.error }, 400);
|
|
347
397
|
return;
|
|
348
398
|
}
|
|
349
|
-
|
|
350
399
|
const currentSessionManager = options.getSessionManager();
|
|
351
400
|
const bodyRecord = asRecord(body);
|
|
352
401
|
const requestedSessionId = String(bodyRecord.sessionId ?? "").trim();
|
|
353
402
|
const runSessionManager = targetSessionManager ?? currentSessionManager;
|
|
354
403
|
const sessionId = runSessionManager.getSessionId();
|
|
355
|
-
if (
|
|
356
|
-
const sessionConflict = chatRunSessionConflict(requestedSessionId, sessionId);
|
|
357
|
-
if (sessionConflict) {
|
|
358
|
-
writeJson(res, sessionConflict, 409);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
} else if (requestedSessionId && requestedSessionId !== sessionId) {
|
|
404
|
+
if (requestedSessionId && requestedSessionId !== sessionId) {
|
|
362
405
|
writeJson(
|
|
363
406
|
res,
|
|
364
407
|
{ error: "Session route and request body disagree", code: "session_changed" },
|
|
@@ -367,9 +410,8 @@ async function handleSseChatRun(
|
|
|
367
410
|
return;
|
|
368
411
|
}
|
|
369
412
|
|
|
370
|
-
const proxyAllowed = bodyOverride === undefined;
|
|
371
413
|
const actionId = String(bodyRecord.actionId ?? "").trim();
|
|
372
|
-
const shouldProxyChatRun =
|
|
414
|
+
const shouldProxyChatRun = allowProxy && canProxyChatRunToCoordinator(runSessionManager);
|
|
373
415
|
if (shouldProxyChatRun) {
|
|
374
416
|
if (await proxyChatRunToCoordinator(res, runSessionManager, bodyRecord)) {
|
|
375
417
|
const useCurrentSession =
|
|
@@ -421,7 +463,7 @@ async function handleSseChatRun(
|
|
|
421
463
|
currentSessionManager,
|
|
422
464
|
runSessionManager,
|
|
423
465
|
sessionId,
|
|
424
|
-
|
|
466
|
+
parsedRun: parsed.value,
|
|
425
467
|
actionId: action.actionId,
|
|
426
468
|
});
|
|
427
469
|
if (!admitted) throw new SessionActionNotAdmitted();
|
|
@@ -449,14 +491,14 @@ async function handleSseChatRun(
|
|
|
449
491
|
currentSessionManager,
|
|
450
492
|
runSessionManager,
|
|
451
493
|
sessionId,
|
|
452
|
-
|
|
494
|
+
parsedRun: parsed.value,
|
|
453
495
|
actionId,
|
|
454
496
|
});
|
|
455
497
|
}
|
|
456
498
|
|
|
457
499
|
export function canProxyChatRunToCoordinator(runSessionManager: SessionManager): boolean {
|
|
458
500
|
const lock = readWriterLock(writerLockScopeForSession(runSessionManager));
|
|
459
|
-
return
|
|
501
|
+
return hasLiveCoordinatorLock(lock);
|
|
460
502
|
}
|
|
461
503
|
|
|
462
504
|
async function proxyChatRunToCoordinator(
|
|
@@ -465,7 +507,7 @@ async function proxyChatRunToCoordinator(
|
|
|
465
507
|
body: Record<string, unknown>,
|
|
466
508
|
): Promise<boolean> {
|
|
467
509
|
const lock = readWriterLock(writerLockScopeForSession(runSessionManager));
|
|
468
|
-
if (!lock
|
|
510
|
+
if (!hasLiveCoordinatorLock(lock)) return false;
|
|
469
511
|
const endpoint = new URL("/api/local-coordinator/chat-run", lock.coordinatorEndpoint);
|
|
470
512
|
let response: Response;
|
|
471
513
|
try {
|
|
@@ -490,6 +532,21 @@ async function proxyChatRunToCoordinator(
|
|
|
490
532
|
return true;
|
|
491
533
|
}
|
|
492
534
|
|
|
535
|
+
function hasLiveCoordinatorLock(
|
|
536
|
+
lock: ReturnType<typeof readWriterLock>,
|
|
537
|
+
staleGraceMs = 15_000,
|
|
538
|
+
): lock is NonNullable<ReturnType<typeof readWriterLock>> & {
|
|
539
|
+
coordinatorEndpoint: string;
|
|
540
|
+
coordinatorSecret: string;
|
|
541
|
+
} {
|
|
542
|
+
if (!lock?.coordinatorEndpoint || !lock.coordinatorSecret || lock.pid === process.pid) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const heartbeat = Date.parse(lock.lastHeartbeat);
|
|
546
|
+
const hasFreshHeartbeat = Number.isFinite(heartbeat) && Date.now() - heartbeat <= staleGraceMs;
|
|
547
|
+
return hasFreshHeartbeat && isCoordinatorOwnerAlive(lock.pid);
|
|
548
|
+
}
|
|
549
|
+
|
|
493
550
|
async function streamAcceptedSseChatRun({
|
|
494
551
|
res,
|
|
495
552
|
options,
|
|
@@ -498,7 +555,7 @@ async function streamAcceptedSseChatRun({
|
|
|
498
555
|
currentSessionManager,
|
|
499
556
|
runSessionManager,
|
|
500
557
|
sessionId,
|
|
501
|
-
|
|
558
|
+
parsedRun,
|
|
502
559
|
actionId,
|
|
503
560
|
}: {
|
|
504
561
|
res: ServerResponse;
|
|
@@ -508,13 +565,37 @@ async function streamAcceptedSseChatRun({
|
|
|
508
565
|
currentSessionManager: SessionManager;
|
|
509
566
|
runSessionManager: SessionManager;
|
|
510
567
|
sessionId: string;
|
|
511
|
-
|
|
568
|
+
parsedRun: ParsedChatRunBody;
|
|
512
569
|
actionId: string;
|
|
513
570
|
}): Promise<boolean> {
|
|
571
|
+
const prompt = parsedRun.prompt;
|
|
514
572
|
if (activeRunSessionIds.has(sessionId)) {
|
|
515
573
|
writeJson(res, { error: "Session already has an active run", code: "session_busy" }, 409);
|
|
516
574
|
return false;
|
|
517
575
|
}
|
|
576
|
+
let dispatchedPrompt: string;
|
|
577
|
+
try {
|
|
578
|
+
dispatchedPrompt = await buildDispatchedPrompt(parsedRun);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
writeJson(res, { error: error instanceof Error ? error.message : String(error) }, 400);
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
const promptImages = parsedRun.images.map((image) => ({
|
|
584
|
+
type: "image" as const,
|
|
585
|
+
data: image.data,
|
|
586
|
+
mimeType: image.mimeType,
|
|
587
|
+
}));
|
|
588
|
+
const attachmentLabels = parsedRun.attachments.map((attachment) => ({
|
|
589
|
+
kind: attachment.kind,
|
|
590
|
+
label: attachmentLabel(attachment),
|
|
591
|
+
}));
|
|
592
|
+
const inputAttachmentLabels = [
|
|
593
|
+
...promptImages.map((image, index) => ({
|
|
594
|
+
kind: "image",
|
|
595
|
+
label: `${image.mimeType || "image"} #${index + 1}`,
|
|
596
|
+
})),
|
|
597
|
+
...attachmentLabels,
|
|
598
|
+
];
|
|
518
599
|
|
|
519
600
|
const currentSessionFile = currentSessionManager.getSessionFile();
|
|
520
601
|
const targetSessionFile = runSessionManager.getSessionFile();
|
|
@@ -579,22 +660,34 @@ async function streamAcceptedSseChatRun({
|
|
|
579
660
|
startSeq: seq,
|
|
580
661
|
emit: (event) => writeSse(res, event),
|
|
581
662
|
originalPrompt: prompt,
|
|
663
|
+
originalAttachments: inputAttachmentLabels,
|
|
582
664
|
});
|
|
583
665
|
const observation = createPromptObservation();
|
|
666
|
+
let originalInputMarkerAppended = false;
|
|
667
|
+
const appendOriginalInputMarker = () => {
|
|
668
|
+
if (inputAttachmentLabels.length === 0 || originalInputMarkerAppended) return;
|
|
669
|
+
runSessionManager.appendCustomEntry("opencandle-user-input", {
|
|
670
|
+
original: prompt,
|
|
671
|
+
attachments: inputAttachmentLabels,
|
|
672
|
+
});
|
|
673
|
+
originalInputMarkerAppended = true;
|
|
674
|
+
};
|
|
584
675
|
let actionAccepted = false;
|
|
585
676
|
const recordAcceptedAction = () => {
|
|
586
677
|
if (actionAccepted) return;
|
|
678
|
+
appendOriginalInputMarker();
|
|
587
679
|
recordAcceptedSessionAction(runSessionManager, actionId);
|
|
588
680
|
if (useCurrentSession) options.syncCurrentWriterLockScope?.();
|
|
589
681
|
actionAccepted = true;
|
|
590
682
|
};
|
|
683
|
+
|
|
591
684
|
const unsubscribeLive = runSession.subscribe((event) => {
|
|
592
685
|
liveAdapter.handle(event);
|
|
593
686
|
observePromptEvent(observation, event);
|
|
594
687
|
if (
|
|
595
688
|
!prompt.startsWith("/") &&
|
|
596
689
|
!actionAccepted &&
|
|
597
|
-
observation.userTexts.some((text) => text.trim() ===
|
|
690
|
+
observation.userTexts.some((text) => text.trim() === dispatchedPrompt.trim())
|
|
598
691
|
) {
|
|
599
692
|
recordAcceptedAction();
|
|
600
693
|
}
|
|
@@ -616,7 +709,13 @@ async function streamAcceptedSseChatRun({
|
|
|
616
709
|
});
|
|
617
710
|
await broadcastRunSessionSnapshot(options, runSessionManager, useCurrentSession);
|
|
618
711
|
} else {
|
|
619
|
-
await promptAndSettle(
|
|
712
|
+
await promptAndSettle(
|
|
713
|
+
runSession,
|
|
714
|
+
dispatchedPrompt,
|
|
715
|
+
beforeIds,
|
|
716
|
+
observation,
|
|
717
|
+
promptImages.length > 0 ? { images: promptImages } : undefined,
|
|
718
|
+
);
|
|
620
719
|
recordAcceptedAction();
|
|
621
720
|
await broadcastRunSessionSnapshot(options, runSessionManager, useCurrentSession);
|
|
622
721
|
}
|
|
@@ -709,7 +808,7 @@ function buildSnapshotPayload(sessionManager: SessionManager): Record<string, un
|
|
|
709
808
|
const entries = sessionManager.getEntries();
|
|
710
809
|
return {
|
|
711
810
|
sessionId,
|
|
712
|
-
state: projectDashboard(entries, sessionId),
|
|
811
|
+
state: projectDashboard(entries, sessionId, getSavedMarketStateSymbols()),
|
|
713
812
|
entries,
|
|
714
813
|
events: sessionEntriesToChatEvents(entries, {
|
|
715
814
|
sessionId,
|
|
@@ -742,7 +841,7 @@ export async function buildSessionBootstrapPayload(
|
|
|
742
841
|
sessions: await SessionManager.list(options.cwd, options.sessionDir),
|
|
743
842
|
snapshot: {
|
|
744
843
|
sessionId,
|
|
745
|
-
state: projectDashboard(entries, sessionId),
|
|
844
|
+
state: projectDashboard(entries, sessionId, getSavedMarketStateSymbols()),
|
|
746
845
|
entries,
|
|
747
846
|
events: sessionEntriesToChatEvents(entries, {
|
|
748
847
|
sessionId,
|
|
@@ -796,20 +895,169 @@ export function buildChatRunActionEnvelope(
|
|
|
796
895
|
body: Record<string, unknown>,
|
|
797
896
|
sessionId: string,
|
|
798
897
|
): SessionActionEnvelope {
|
|
799
|
-
const actionId = String(body.actionId ?? "").trim()
|
|
898
|
+
const actionId = String(body.actionId ?? "").trim();
|
|
899
|
+
const parsed = parseChatRunBody(body);
|
|
900
|
+
const attachments = parsed.ok ? parsed.value.attachments : [];
|
|
901
|
+
const images = parsed.ok ? parsed.value.images : [];
|
|
800
902
|
return {
|
|
801
903
|
sessionId,
|
|
802
904
|
actionId,
|
|
803
905
|
actionType: "chat.prompt",
|
|
804
|
-
payload: {
|
|
906
|
+
payload: {
|
|
907
|
+
prompt: String(body.prompt ?? ""),
|
|
908
|
+
...(images.length > 0 ? { imageCount: images.length } : {}),
|
|
909
|
+
...(attachments.length > 0
|
|
910
|
+
? { attachments: attachments.map((attachment) => ({ kind: attachment.kind })) }
|
|
911
|
+
: {}),
|
|
912
|
+
},
|
|
805
913
|
source: "browser",
|
|
806
914
|
};
|
|
807
915
|
}
|
|
808
916
|
|
|
917
|
+
export function parseChatRunBody(body: unknown): ChatRunParseResult {
|
|
918
|
+
const record = asRecord(body);
|
|
919
|
+
const prompt = String(record.prompt ?? "").trim();
|
|
920
|
+
if (!prompt) return { ok: false, error: "prompt is required" };
|
|
921
|
+
|
|
922
|
+
const rawImages = record.images;
|
|
923
|
+
const images: ChatRunImageInput[] = [];
|
|
924
|
+
if (rawImages !== undefined) {
|
|
925
|
+
if (!Array.isArray(rawImages)) return { ok: false, error: "images must be an array" };
|
|
926
|
+
if (rawImages.length > CHAT_RUN_MAX_IMAGE_COUNT) {
|
|
927
|
+
return { ok: false, error: "Attach up to 4 images" };
|
|
928
|
+
}
|
|
929
|
+
for (const rawImage of rawImages) {
|
|
930
|
+
const image = asRecord(rawImage);
|
|
931
|
+
const mimeType = String(image.mimeType ?? "");
|
|
932
|
+
const data = String(image.data ?? "");
|
|
933
|
+
if (!CHAT_RUN_IMAGE_MIME_TYPES.has(mimeType)) {
|
|
934
|
+
return { ok: false, error: "Unsupported image mime type" };
|
|
935
|
+
}
|
|
936
|
+
if (data.length > Math.ceil(CHAT_RUN_MAX_IMAGE_BYTES / 3) * 4) {
|
|
937
|
+
return { ok: false, error: "Image attachment must be 5 MB or smaller" };
|
|
938
|
+
}
|
|
939
|
+
const decoded = decodeStrictBase64(data);
|
|
940
|
+
if (!decoded) {
|
|
941
|
+
return { ok: false, error: "Image attachment data must be valid base64" };
|
|
942
|
+
}
|
|
943
|
+
if (decoded.byteLength > CHAT_RUN_MAX_IMAGE_BYTES) {
|
|
944
|
+
return { ok: false, error: "Image attachment must be 5 MB or smaller" };
|
|
945
|
+
}
|
|
946
|
+
images.push({ data, mimeType });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const rawAttachments = record.attachments;
|
|
951
|
+
const attachments: ChatRunAttachmentInput[] = [];
|
|
952
|
+
if (rawAttachments !== undefined) {
|
|
953
|
+
if (!Array.isArray(rawAttachments)) {
|
|
954
|
+
return { ok: false, error: "attachments must be an array" };
|
|
955
|
+
}
|
|
956
|
+
for (const rawAttachment of rawAttachments) {
|
|
957
|
+
const attachment = asRecord(rawAttachment);
|
|
958
|
+
const kind = String(attachment.kind ?? "");
|
|
959
|
+
const id = typeof attachment.id === "string" ? attachment.id.trim() : "";
|
|
960
|
+
if (kind === "portfolio") {
|
|
961
|
+
attachments.push(id ? { kind, id } : { kind });
|
|
962
|
+
} else if (kind === "watchlist" || kind === "report") {
|
|
963
|
+
if (!id) return { ok: false, error: `${kind} attachment id is required` };
|
|
964
|
+
attachments.push({ kind, id });
|
|
965
|
+
} else {
|
|
966
|
+
return { ok: false, error: "Unsupported attachment kind" };
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (prompt.startsWith("/") && (images.length > 0 || attachments.length > 0)) {
|
|
972
|
+
return { ok: false, error: "Attachments are not supported for slash commands" };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return { ok: true, value: { prompt, images, attachments } };
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function decodeStrictBase64(data: string): Buffer | null {
|
|
979
|
+
if (!data || data.trim() !== data) return null;
|
|
980
|
+
if (data.length % 4 !== 0) return null;
|
|
981
|
+
if (/[^A-Za-z0-9+/=]/.test(data)) return null;
|
|
982
|
+
const firstPadding = data.indexOf("=");
|
|
983
|
+
if (firstPadding !== -1) {
|
|
984
|
+
const padding = data.slice(firstPadding);
|
|
985
|
+
if (!/^={1,2}$/.test(padding)) return null;
|
|
986
|
+
}
|
|
987
|
+
const decoded = Buffer.from(data, "base64");
|
|
988
|
+
if (decoded.byteLength === 0) return null;
|
|
989
|
+
return decoded.toString("base64") === data ? decoded : null;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
export async function buildDispatchedPrompt(parsed: ParsedChatRunBody): Promise<string> {
|
|
993
|
+
if (parsed.prompt.startsWith("/")) return parsed.prompt;
|
|
994
|
+
if (parsed.attachments.length === 0) return parsed.prompt;
|
|
995
|
+
const db = initDefaultDatabase();
|
|
996
|
+
try {
|
|
997
|
+
const service = new MarketStateService(db);
|
|
998
|
+
const blocks = parsed.attachments.map((attachment) => {
|
|
999
|
+
const lines = attachmentSummaryLines(service, attachment);
|
|
1000
|
+
if (lines.length === 0) throw new Error(`${attachment.kind} attachment was empty`);
|
|
1001
|
+
return `[Attached by user — ${attachment.kind}]\n${lines.join("\n")}`;
|
|
1002
|
+
});
|
|
1003
|
+
return [parsed.prompt, ...blocks].join("\n\n");
|
|
1004
|
+
} finally {
|
|
1005
|
+
db.close();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function attachmentSummaryLines(
|
|
1010
|
+
service: MarketStateService,
|
|
1011
|
+
attachment: ChatRunAttachmentInput,
|
|
1012
|
+
): string[] {
|
|
1013
|
+
if (attachment.kind === "portfolio") {
|
|
1014
|
+
const portfolioId = attachment.id ? Number(attachment.id) : undefined;
|
|
1015
|
+
if (attachment.id && !Number.isFinite(portfolioId)) {
|
|
1016
|
+
throw new Error("Unknown portfolio attachment");
|
|
1017
|
+
}
|
|
1018
|
+
return formatPortfolioSummary(service.listPortfolioLots(portfolioId));
|
|
1019
|
+
}
|
|
1020
|
+
if (attachment.kind === "watchlist") {
|
|
1021
|
+
const watchlistId =
|
|
1022
|
+
attachment.id === "default" ? service.getDefaultWatchlist().id : Number(attachment.id);
|
|
1023
|
+
if (!Number.isFinite(watchlistId)) throw new Error("Unknown watchlist attachment");
|
|
1024
|
+
return formatWatchlistSummary(service.listWatchlistItems(watchlistId));
|
|
1025
|
+
}
|
|
1026
|
+
const reports = service.listReportRuns();
|
|
1027
|
+
const report =
|
|
1028
|
+
attachment.id === "latest"
|
|
1029
|
+
? reports[0]
|
|
1030
|
+
: reports.find((candidate) => candidate.id === Number(attachment.id));
|
|
1031
|
+
if (!report) throw new Error("Unknown report attachment");
|
|
1032
|
+
return formatLatestReportSummary(report);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function attachmentLabel(attachment: ChatRunAttachmentInput): string {
|
|
1036
|
+
if (attachment.kind === "portfolio") return "Portfolio";
|
|
1037
|
+
if (attachment.kind === "watchlist") return "Watchlist";
|
|
1038
|
+
return "Latest report";
|
|
1039
|
+
}
|
|
1040
|
+
|
|
809
1041
|
function hasClientActionId(body: Record<string, unknown>): boolean {
|
|
810
1042
|
return typeof body.actionId === "string" && body.actionId.trim().length > 0;
|
|
811
1043
|
}
|
|
812
1044
|
|
|
1045
|
+
function requireSessionActionFields(
|
|
1046
|
+
res: ServerResponse,
|
|
1047
|
+
body: Record<string, unknown>,
|
|
1048
|
+
requireSessionId = true,
|
|
1049
|
+
): boolean {
|
|
1050
|
+
if (requireSessionId && !String(body.sessionId ?? "").trim()) {
|
|
1051
|
+
writeJson(res, { error: "sessionId is required" }, 400);
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
if (!String(body.actionId ?? "").trim()) {
|
|
1055
|
+
writeJson(res, { error: "actionId is required" }, 400);
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
813
1061
|
function shouldBlockFailedCoordinatorAction(
|
|
814
1062
|
runSessionManager: SessionManager,
|
|
815
1063
|
body: Record<string, unknown>,
|
|
@@ -194,21 +194,22 @@ export function createToolInvokeController({
|
|
|
194
194
|
const sessionId = typeof data.sessionId === "string" ? data.sessionId : "";
|
|
195
195
|
const allowProxy = data.allowProxy !== false;
|
|
196
196
|
try {
|
|
197
|
+
if (!sessionId.trim()) throw new Error("sessionId is required");
|
|
198
|
+
if (!actionId.trim()) throw new Error("actionId is required");
|
|
197
199
|
const invoke = () =>
|
|
198
200
|
handleToolInvoke(toolName, requestArgs(data.args), sessionId, { actionId, allowProxy });
|
|
199
|
-
const actionResult =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
: await invoke().then((result) => ({ ok: true as const, duplicate: false, result }));
|
|
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 }));
|
|
212
213
|
if (!actionResult.ok) throw new Error(actionResult.message);
|
|
213
214
|
const result = actionResult.result;
|
|
214
215
|
if (requestId) {
|
|
@@ -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 {
|