gloomberb 0.12.0 → 0.13.2
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 +2 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/index.ts +10 -2
- package/src/api-client/request.ts +4 -1
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +9 -5
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +195 -42
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +63 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +32 -18
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sources/gloomberb-cloud/news.ts +1 -0
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { MarketContext } from "../../../../cli/types";
|
|
3
|
+
import type { PaneFunctionCatalog } from "../../../../cli/pane-functions/catalog";
|
|
4
|
+
import type { RemoteControlResponse } from "../../../../remote/types";
|
|
5
|
+
import {
|
|
6
|
+
MAX_TOOL_RESULT_BYTES,
|
|
7
|
+
type ASKGToolCallEvent,
|
|
8
|
+
type ClientToolManifest,
|
|
9
|
+
} from "./protocol";
|
|
10
|
+
import { createASKGToolExecutor } from "./executor";
|
|
11
|
+
|
|
12
|
+
const emptyRegistry: PaneFunctionCatalog = {
|
|
13
|
+
panes: new Map(),
|
|
14
|
+
paneTemplates: new Map(),
|
|
15
|
+
destroy() {},
|
|
16
|
+
};
|
|
17
|
+
const emptyContext = {} as MarketContext;
|
|
18
|
+
|
|
19
|
+
const headlessManifest: ClientToolManifest = {
|
|
20
|
+
name: "val",
|
|
21
|
+
source: "headless",
|
|
22
|
+
title: "Valuation",
|
|
23
|
+
description: "Read valuation data.",
|
|
24
|
+
writeTier: "read",
|
|
25
|
+
shape: "rows",
|
|
26
|
+
argument: { kind: "none" },
|
|
27
|
+
options: [],
|
|
28
|
+
confirm: "never",
|
|
29
|
+
timeoutMs: 30_000,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const resourceManifest: ClientToolManifest = {
|
|
33
|
+
name: "app.get_resource",
|
|
34
|
+
source: "remote-op",
|
|
35
|
+
title: "App: Get resource",
|
|
36
|
+
description: "Read an app resource.",
|
|
37
|
+
writeTier: "read",
|
|
38
|
+
inputSchema: { type: "object" },
|
|
39
|
+
confirm: "never",
|
|
40
|
+
timeoutMs: 10_000,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function call(
|
|
44
|
+
manifest: ClientToolManifest,
|
|
45
|
+
overrides: Partial<ASKGToolCallEvent> = {},
|
|
46
|
+
): ASKGToolCallEvent {
|
|
47
|
+
return {
|
|
48
|
+
seq: 1,
|
|
49
|
+
type: "tool-call",
|
|
50
|
+
turnId: "turn-1",
|
|
51
|
+
toolCallId: "tool-1",
|
|
52
|
+
name: manifest.name,
|
|
53
|
+
args: {},
|
|
54
|
+
writeTier: manifest.writeTier,
|
|
55
|
+
requiresConfirmation: false,
|
|
56
|
+
preview: null,
|
|
57
|
+
timeoutMs: 1_000,
|
|
58
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
59
|
+
...overrides,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function executor(options: {
|
|
64
|
+
manifests?: ClientToolManifest[];
|
|
65
|
+
remoteHandler?: (request: unknown) => Promise<RemoteControlResponse>;
|
|
66
|
+
headlessExecutor?: Parameters<typeof createASKGToolExecutor>[0]["headlessExecutor"];
|
|
67
|
+
}) {
|
|
68
|
+
return createASKGToolExecutor({
|
|
69
|
+
manifests: options.manifests ?? [headlessManifest],
|
|
70
|
+
registry: emptyRegistry,
|
|
71
|
+
context: emptyContext,
|
|
72
|
+
remoteHandler: options.remoteHandler ?? (async () => ({ ok: true, data: {} })),
|
|
73
|
+
...(options.headlessExecutor ? { headlessExecutor: options.headlessExecutor } : {}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("ASKG delegated tool executor", () => {
|
|
78
|
+
test("denies a server tier that does not match the advertised tier", async () => {
|
|
79
|
+
let executions = 0;
|
|
80
|
+
const tools = executor({
|
|
81
|
+
headlessExecutor: async () => {
|
|
82
|
+
executions += 1;
|
|
83
|
+
return { result: { rows: [] }, rowCount: 0 };
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await tools.execute(call(headlessManifest, { writeTier: "ui-write" }));
|
|
88
|
+
|
|
89
|
+
expect(result.status).toBe("denied");
|
|
90
|
+
expect(result.note).toContain("tier mismatch");
|
|
91
|
+
expect(executions).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("returns timeout without waiting for a stalled loader", async () => {
|
|
95
|
+
const tools = executor({
|
|
96
|
+
headlessExecutor: async () => new Promise(() => {}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = await tools.execute(call(headlessManifest, { timeoutMs: 5 }));
|
|
100
|
+
|
|
101
|
+
expect(result.status).toBe("timeout");
|
|
102
|
+
expect(result.elapsedMs).toBeLessThan(500);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("cancels an in-flight delegated load", async () => {
|
|
106
|
+
const started = Promise.withResolvers<void>();
|
|
107
|
+
const tools = executor({
|
|
108
|
+
headlessExecutor: async () => {
|
|
109
|
+
started.resolve();
|
|
110
|
+
return new Promise(() => {});
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const pending = tools.execute(call(headlessManifest), { signal: controller.signal });
|
|
115
|
+
await started.promise;
|
|
116
|
+
controller.abort();
|
|
117
|
+
|
|
118
|
+
const result = await pending;
|
|
119
|
+
|
|
120
|
+
expect(result.status).toBe("cancelled");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("truncates the complete wire payload under the byte cap", async () => {
|
|
124
|
+
const tools = executor({
|
|
125
|
+
manifests: [resourceManifest],
|
|
126
|
+
remoteHandler: async () => ({ ok: true, data: { rows: [{ text: "x".repeat(MAX_TOOL_RESULT_BYTES * 2) }] } }),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await tools.execute(call(resourceManifest, {
|
|
130
|
+
args: { resource: "app://snapshot" },
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
expect(result.status).toBe("partial");
|
|
134
|
+
expect(result.truncated).toBe(true);
|
|
135
|
+
expect(result.rowCount).toBe(1);
|
|
136
|
+
expect(new TextEncoder().encode(JSON.stringify(result)).byteLength).toBeLessThanOrEqual(MAX_TOOL_RESULT_BYTES);
|
|
137
|
+
expect(result.result).toMatchObject({ originalBytes: expect.any(Number) });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("returns remote failures as error values", async () => {
|
|
141
|
+
const tools = executor({
|
|
142
|
+
manifests: [resourceManifest],
|
|
143
|
+
remoteHandler: async () => ({
|
|
144
|
+
ok: false,
|
|
145
|
+
error: { code: "upstream_unavailable", message: "Upstream source is unavailable." },
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = await tools.execute(call(resourceManifest, {
|
|
150
|
+
args: { resource: "app://snapshot" },
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
expect(result).toMatchObject({
|
|
154
|
+
status: "error",
|
|
155
|
+
result: { code: "upstream_unavailable" },
|
|
156
|
+
note: "Upstream source is unavailable.",
|
|
157
|
+
truncated: false,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import type { MarketContext } from "../../../../cli/types";
|
|
2
|
+
import type { PaneFunctionCatalog } from "../../../../cli/pane-functions/catalog";
|
|
3
|
+
import { withPersistedCloudSession } from "../../../../cli/pane-functions/cloud-session";
|
|
4
|
+
import {
|
|
5
|
+
loadResolvedHeadlessPaneModel,
|
|
6
|
+
serializeHeadlessPaneResult,
|
|
7
|
+
} from "../../../../cli/pane-functions/headless";
|
|
8
|
+
import { resolvePaneFunction } from "../../../../cli/pane-functions/resolver";
|
|
9
|
+
import { stableStringify } from "../../../../remote/revision";
|
|
10
|
+
import type {
|
|
11
|
+
HeadlessBundleResult,
|
|
12
|
+
HeadlessPaneDefinition,
|
|
13
|
+
HeadlessPaneResult,
|
|
14
|
+
HeadlessSeriesResult,
|
|
15
|
+
HeadlessSnapshotResult,
|
|
16
|
+
} from "../../../../types/headless";
|
|
17
|
+
import {
|
|
18
|
+
MAX_TOOL_RESULT_BYTES,
|
|
19
|
+
type ASKGToolCallEvent,
|
|
20
|
+
type ClientToolManifest,
|
|
21
|
+
type JsonValue,
|
|
22
|
+
type ToolResultPayload,
|
|
23
|
+
type ToolResultStatus,
|
|
24
|
+
} from "./protocol";
|
|
25
|
+
import {
|
|
26
|
+
remoteRequestForTool,
|
|
27
|
+
resolveRemoteToolBinding,
|
|
28
|
+
} from "./manifest";
|
|
29
|
+
import {
|
|
30
|
+
ASKGUndoManager,
|
|
31
|
+
type AppliedUndo,
|
|
32
|
+
type InProcessRemoteControlHandler,
|
|
33
|
+
} from "./undo";
|
|
34
|
+
|
|
35
|
+
interface HeadlessExecution {
|
|
36
|
+
result: unknown;
|
|
37
|
+
rowCount: number;
|
|
38
|
+
errors?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HeadlessToolCall {
|
|
42
|
+
name: string;
|
|
43
|
+
args: Record<string, JsonValue>;
|
|
44
|
+
manifest: ClientToolManifest;
|
|
45
|
+
signal: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type HeadlessToolExecutor = (call: HeadlessToolCall) => Promise<HeadlessExecution>;
|
|
49
|
+
|
|
50
|
+
export interface ASKGToolExecutorDependencies {
|
|
51
|
+
manifests: readonly ClientToolManifest[];
|
|
52
|
+
registry: PaneFunctionCatalog;
|
|
53
|
+
context: MarketContext;
|
|
54
|
+
remoteHandler: InProcessRemoteControlHandler;
|
|
55
|
+
headlessExecutor?: HeadlessToolExecutor;
|
|
56
|
+
undoManager?: ASKGUndoManager;
|
|
57
|
+
now?: () => number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ASKGToolExecutionOptions {
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
confirmed?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ASKGToolExecutor {
|
|
66
|
+
execute(
|
|
67
|
+
call: ASKGToolCallEvent,
|
|
68
|
+
options?: ASKGToolExecutionOptions,
|
|
69
|
+
): Promise<ToolResultPayload>;
|
|
70
|
+
undo(token: string, signal?: AbortSignal): Promise<AppliedUndo>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ExecutionValue {
|
|
74
|
+
status: ToolResultStatus;
|
|
75
|
+
result?: unknown;
|
|
76
|
+
rowCount?: number;
|
|
77
|
+
truncated?: boolean;
|
|
78
|
+
note?: string;
|
|
79
|
+
rev?: string;
|
|
80
|
+
undoToken?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ARGUMENT_KEYS = new Set(["symbol", "symbols", "text"]);
|
|
84
|
+
|
|
85
|
+
class ToolTimeoutError extends Error {}
|
|
86
|
+
class ToolCancelledError extends Error {}
|
|
87
|
+
|
|
88
|
+
function shortReason(error: unknown): string {
|
|
89
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
90
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 320) || "Unknown error.";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function appendNote(current: string | undefined, next: string): string {
|
|
94
|
+
return current ? `${current} ${next}` : next;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeJson(value: unknown, seen = new WeakSet<object>()): JsonValue {
|
|
98
|
+
if (value == null) return null;
|
|
99
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
100
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
101
|
+
if (typeof value === "bigint") return value.toString();
|
|
102
|
+
if (value instanceof Date) return value.toISOString();
|
|
103
|
+
if (value instanceof Error) return { name: value.name, message: value.message };
|
|
104
|
+
if (typeof value !== "object") return String(value);
|
|
105
|
+
if (seen.has(value)) return "[Circular]";
|
|
106
|
+
seen.add(value);
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
const result = value.map((entry) => normalizeJson(entry, seen));
|
|
109
|
+
seen.delete(value);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
const result: Record<string, JsonValue> = {};
|
|
113
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
114
|
+
result[key] = normalizeJson((value as Record<string, unknown>)[key], seen);
|
|
115
|
+
}
|
|
116
|
+
seen.delete(value);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function encodedSize(value: unknown): number {
|
|
121
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function withBoundedPayload(payload: ToolResultPayload): ToolResultPayload {
|
|
125
|
+
if (encodedSize(payload) <= MAX_TOOL_RESULT_BYTES) return payload;
|
|
126
|
+
|
|
127
|
+
const resultText = stableStringify(payload.result ?? null);
|
|
128
|
+
const originalBytes = encodedSize(payload.result ?? null);
|
|
129
|
+
const note = appendNote(
|
|
130
|
+
payload.note,
|
|
131
|
+
`Result exceeded ${MAX_TOOL_RESULT_BYTES} bytes. result.jsonPreview contains a canonical JSON prefix.`,
|
|
132
|
+
);
|
|
133
|
+
const status = payload.status === "ok" ? "partial" : payload.status;
|
|
134
|
+
let low = 0;
|
|
135
|
+
let high = resultText.length;
|
|
136
|
+
let best: ToolResultPayload = {
|
|
137
|
+
...payload,
|
|
138
|
+
status,
|
|
139
|
+
result: { jsonPreview: "", originalBytes },
|
|
140
|
+
truncated: true,
|
|
141
|
+
note,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
while (low <= high) {
|
|
145
|
+
const middle = Math.floor((low + high) / 2);
|
|
146
|
+
const candidate: ToolResultPayload = {
|
|
147
|
+
...best,
|
|
148
|
+
result: { jsonPreview: resultText.slice(0, middle), originalBytes },
|
|
149
|
+
};
|
|
150
|
+
if (encodedSize(candidate) <= MAX_TOOL_RESULT_BYTES) {
|
|
151
|
+
best = candidate;
|
|
152
|
+
low = middle + 1;
|
|
153
|
+
} else {
|
|
154
|
+
high = middle - 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (encodedSize(best) <= MAX_TOOL_RESULT_BYTES) return best;
|
|
159
|
+
const withoutResult: ToolResultPayload = {
|
|
160
|
+
turnId: payload.turnId,
|
|
161
|
+
toolCallId: payload.toolCallId,
|
|
162
|
+
status,
|
|
163
|
+
rowCount: payload.rowCount,
|
|
164
|
+
truncated: true,
|
|
165
|
+
elapsedMs: payload.elapsedMs,
|
|
166
|
+
note: "Result exceeded the client payload limit and was omitted.",
|
|
167
|
+
...(payload.rev ? { rev: payload.rev } : {}),
|
|
168
|
+
...(payload.undoToken ? { undoToken: payload.undoToken } : {}),
|
|
169
|
+
};
|
|
170
|
+
return withoutResult;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function rawArgumentFor(manifest: ClientToolManifest, args: Record<string, JsonValue>): string {
|
|
174
|
+
switch (manifest.argument?.kind) {
|
|
175
|
+
case "none":
|
|
176
|
+
case undefined:
|
|
177
|
+
return "";
|
|
178
|
+
case "ticker": {
|
|
179
|
+
const symbol = args.symbol;
|
|
180
|
+
if (symbol == null && manifest.argument.optional) return "";
|
|
181
|
+
if (typeof symbol !== "string") throw new Error(`${manifest.name} requires a symbol string.`);
|
|
182
|
+
return symbol;
|
|
183
|
+
}
|
|
184
|
+
case "tickers":
|
|
185
|
+
case "symbol-list": {
|
|
186
|
+
const symbols = args.symbols;
|
|
187
|
+
if (symbols == null && manifest.argument.optional) return "";
|
|
188
|
+
if (!Array.isArray(symbols) || !symbols.every((symbol) => typeof symbol === "string")) {
|
|
189
|
+
throw new Error(`${manifest.name} requires a symbols array.`);
|
|
190
|
+
}
|
|
191
|
+
return symbols.join(",");
|
|
192
|
+
}
|
|
193
|
+
case "free-text": {
|
|
194
|
+
const text = args.text;
|
|
195
|
+
if (text == null && manifest.argument.optional) return "";
|
|
196
|
+
if (typeof text !== "string") throw new Error(`${manifest.name} requires a text string.`);
|
|
197
|
+
return text;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function optionArgsFor(manifest: ClientToolManifest, args: Record<string, JsonValue>): Record<string, string | true> {
|
|
203
|
+
const declared = new Set(manifest.options?.map(({ key }) => key) ?? []);
|
|
204
|
+
const options: Record<string, string | true> = {};
|
|
205
|
+
for (const [key, value] of Object.entries(args)) {
|
|
206
|
+
if (ARGUMENT_KEYS.has(key)) continue;
|
|
207
|
+
if (!declared.has(key)) throw new Error(`Unknown option "${key}" for ${manifest.name}.`);
|
|
208
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
209
|
+
throw new Error(`Option "${key}" for ${manifest.name} must be a scalar value.`);
|
|
210
|
+
}
|
|
211
|
+
options[key] = value === true ? true : String(value);
|
|
212
|
+
}
|
|
213
|
+
return options;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function headlessRowCount(definition: HeadlessPaneDefinition, result: HeadlessPaneResult): number {
|
|
217
|
+
switch (definition.shape) {
|
|
218
|
+
case "rows":
|
|
219
|
+
return (result as { rows: unknown[] }).rows.length;
|
|
220
|
+
case "bundle":
|
|
221
|
+
return (result as HeadlessBundleResult).sections.reduce((count, section) => (
|
|
222
|
+
count + ("rows" in section && section.rows ? section.rows.length : section.entries.length)
|
|
223
|
+
), 0);
|
|
224
|
+
case "series":
|
|
225
|
+
return (result as HeadlessSeriesResult).series.reduce(
|
|
226
|
+
(count, series) => count + series.points.length,
|
|
227
|
+
0,
|
|
228
|
+
);
|
|
229
|
+
case "snapshot":
|
|
230
|
+
return (result as HeadlessSnapshotResult).items.length;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function inferRemoteRowCount(value: unknown): number | undefined {
|
|
235
|
+
if (Array.isArray(value)) return value.length;
|
|
236
|
+
if (value == null || typeof value !== "object") return undefined;
|
|
237
|
+
const record = value as Record<string, unknown>;
|
|
238
|
+
for (const key of ["rows", "items", "panes", "sections", "series", "results"]) {
|
|
239
|
+
if (Array.isArray(record[key])) return record[key].length;
|
|
240
|
+
}
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function effectiveTimeoutMs(call: ASKGToolCallEvent, now: number): number {
|
|
245
|
+
const declared = Math.max(0, call.timeoutMs);
|
|
246
|
+
const expiresAt = Date.parse(call.expiresAt);
|
|
247
|
+
if (!Number.isFinite(expiresAt)) return declared;
|
|
248
|
+
return Math.max(0, Math.min(declared, expiresAt - now));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function runWithDeadline<T>(
|
|
252
|
+
run: (signal: AbortSignal) => Promise<T>,
|
|
253
|
+
timeoutMs: number,
|
|
254
|
+
externalSignal?: AbortSignal,
|
|
255
|
+
): Promise<T> {
|
|
256
|
+
if (externalSignal?.aborted) throw new ToolCancelledError("Tool call was cancelled.");
|
|
257
|
+
if (timeoutMs <= 0) throw new ToolTimeoutError("Tool call timed out.");
|
|
258
|
+
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
261
|
+
let removeAbortListener: (() => void) | undefined;
|
|
262
|
+
const gate = new Promise<never>((_resolve, reject) => {
|
|
263
|
+
timeoutHandle = setTimeout(() => {
|
|
264
|
+
const error = new ToolTimeoutError("Tool call timed out.");
|
|
265
|
+
controller.abort(error);
|
|
266
|
+
reject(error);
|
|
267
|
+
}, timeoutMs);
|
|
268
|
+
if (externalSignal) {
|
|
269
|
+
const onAbort = () => {
|
|
270
|
+
const error = new ToolCancelledError("Tool call was cancelled.");
|
|
271
|
+
controller.abort(error);
|
|
272
|
+
reject(error);
|
|
273
|
+
};
|
|
274
|
+
externalSignal.addEventListener("abort", onAbort, { once: true });
|
|
275
|
+
removeAbortListener = () => externalSignal.removeEventListener("abort", onAbort);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
return await Promise.race([run(controller.signal), gate]);
|
|
281
|
+
} finally {
|
|
282
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
283
|
+
removeAbortListener?.();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function defaultHeadlessExecutor(
|
|
288
|
+
registry: PaneFunctionCatalog,
|
|
289
|
+
context: MarketContext,
|
|
290
|
+
): HeadlessToolExecutor {
|
|
291
|
+
return async ({ name, args, manifest, signal }) => {
|
|
292
|
+
const rawArgument = rawArgumentFor(manifest, args);
|
|
293
|
+
const resolved = await resolvePaneFunction(registry, context, {
|
|
294
|
+
target: name,
|
|
295
|
+
arg: rawArgument,
|
|
296
|
+
options: optionArgsFor(manifest, args),
|
|
297
|
+
outputPath: null,
|
|
298
|
+
width: 1280,
|
|
299
|
+
height: 720,
|
|
300
|
+
theme: null,
|
|
301
|
+
scale: 1,
|
|
302
|
+
watermark: null,
|
|
303
|
+
requireBotSafe: false,
|
|
304
|
+
}, { strictHeadlessOptions: true });
|
|
305
|
+
const loaded = await withPersistedCloudSession(
|
|
306
|
+
context,
|
|
307
|
+
() => loadResolvedHeadlessPaneModel(resolved, context, rawArgument, signal),
|
|
308
|
+
);
|
|
309
|
+
return {
|
|
310
|
+
result: serializeHeadlessPaneResult(loaded.definition, loaded.result),
|
|
311
|
+
rowCount: headlessRowCount(loaded.definition, loaded.result),
|
|
312
|
+
...(loaded.result.errors?.length ? { errors: loaded.result.errors } : {}),
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function createASKGToolExecutor(
|
|
318
|
+
dependencies: ASKGToolExecutorDependencies,
|
|
319
|
+
): ASKGToolExecutor {
|
|
320
|
+
const manifests = new Map(dependencies.manifests.map((manifest) => [manifest.name, manifest]));
|
|
321
|
+
const now = dependencies.now ?? Date.now;
|
|
322
|
+
const executeHeadless = dependencies.headlessExecutor
|
|
323
|
+
?? defaultHeadlessExecutor(dependencies.registry, dependencies.context);
|
|
324
|
+
const undoManager = dependencies.undoManager ?? new ASKGUndoManager(dependencies.remoteHandler);
|
|
325
|
+
|
|
326
|
+
const execute = async (
|
|
327
|
+
call: ASKGToolCallEvent,
|
|
328
|
+
options: ASKGToolExecutionOptions = {},
|
|
329
|
+
): Promise<ToolResultPayload> => {
|
|
330
|
+
const startedAt = now();
|
|
331
|
+
const base = (value: ExecutionValue): ToolResultPayload => withBoundedPayload({
|
|
332
|
+
turnId: call.turnId,
|
|
333
|
+
toolCallId: call.toolCallId,
|
|
334
|
+
status: value.status,
|
|
335
|
+
...(value.result !== undefined ? { result: normalizeJson(value.result) } : {}),
|
|
336
|
+
...(value.rowCount !== undefined ? { rowCount: value.rowCount } : {}),
|
|
337
|
+
truncated: value.truncated ?? false,
|
|
338
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
339
|
+
...(value.note ? { note: value.note } : {}),
|
|
340
|
+
...(value.rev ? { rev: value.rev } : {}),
|
|
341
|
+
...(value.undoToken ? { undoToken: value.undoToken } : {}),
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const manifest = manifests.get(call.name);
|
|
345
|
+
if (!manifest) {
|
|
346
|
+
return base({ status: "denied", note: `Tool "${call.name}" was not advertised by this client.` });
|
|
347
|
+
}
|
|
348
|
+
if (call.writeTier !== manifest.writeTier) {
|
|
349
|
+
return base({
|
|
350
|
+
status: "denied",
|
|
351
|
+
note: `Tool tier mismatch: advertised ${manifest.writeTier}, received ${call.writeTier}.`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if ((manifest.confirm === "always" || call.requiresConfirmation) && !options.confirmed) {
|
|
355
|
+
return base({ status: "denied", note: "This tool call requires local confirmation." });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const timeoutMs = Math.min(manifest.timeoutMs, effectiveTimeoutMs(call, now()));
|
|
359
|
+
try {
|
|
360
|
+
const value = await runWithDeadline(async (signal): Promise<ExecutionValue> => {
|
|
361
|
+
if (manifest.source === "headless") {
|
|
362
|
+
const loaded = await executeHeadless({ name: call.name, args: call.args, manifest, signal });
|
|
363
|
+
const errors = loaded.errors?.filter(Boolean) ?? [];
|
|
364
|
+
return {
|
|
365
|
+
status: errors.length > 0 ? (loaded.rowCount > 0 ? "partial" : "error") : "ok",
|
|
366
|
+
result: loaded.result,
|
|
367
|
+
rowCount: loaded.rowCount,
|
|
368
|
+
...(errors.length > 0 ? {
|
|
369
|
+
truncated: loaded.rowCount > 0,
|
|
370
|
+
note: errors.map(shortReason).join(" "),
|
|
371
|
+
} : {}),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const binding = resolveRemoteToolBinding(call.name);
|
|
376
|
+
if (!binding) throw new Error(`Remote tool "${call.name}" has no in-process binding.`);
|
|
377
|
+
let preparedUndo: Awaited<ReturnType<ASKGUndoManager["prepare"]>> = null;
|
|
378
|
+
let undoNote: string | undefined;
|
|
379
|
+
if (manifest.writeTier === "ui-write" && binding.kind === "operation") {
|
|
380
|
+
try {
|
|
381
|
+
preparedUndo = await undoManager.prepare(binding.operation, call.args, signal);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
undoNote = `Undo is unavailable: ${shortReason(error)}`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const response = await dependencies.remoteHandler(remoteRequestForTool(binding, call.args));
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
return {
|
|
390
|
+
status: "error",
|
|
391
|
+
result: { code: response.error.code, ...(response.error.details !== undefined ? { details: response.error.details } : {}) },
|
|
392
|
+
note: shortReason(response.error.message),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let undoToken: string | undefined;
|
|
397
|
+
if (preparedUndo) {
|
|
398
|
+
try {
|
|
399
|
+
undoToken = await undoManager.commit(preparedUndo, signal) ?? undefined;
|
|
400
|
+
} catch (error) {
|
|
401
|
+
undoNote = `Undo is unavailable: ${shortReason(error)}`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
status: "ok",
|
|
406
|
+
result: response.data,
|
|
407
|
+
rowCount: inferRemoteRowCount(response.data),
|
|
408
|
+
...(undoNote ? { note: undoNote } : {}),
|
|
409
|
+
...(response.rev ? { rev: response.rev } : response.state?.rev ? { rev: response.state.rev } : {}),
|
|
410
|
+
...(undoToken ? { undoToken } : {}),
|
|
411
|
+
};
|
|
412
|
+
}, timeoutMs, options.signal);
|
|
413
|
+
return base(value);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (error instanceof ToolTimeoutError) {
|
|
416
|
+
return base({ status: "timeout", note: "Tool call timed out." });
|
|
417
|
+
}
|
|
418
|
+
if (error instanceof ToolCancelledError || options.signal?.aborted) {
|
|
419
|
+
return base({ status: "cancelled", note: "Tool call was cancelled." });
|
|
420
|
+
}
|
|
421
|
+
return base({ status: "error", note: shortReason(error) });
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
execute,
|
|
427
|
+
undo: (token, signal) => undoManager.undo(token, signal),
|
|
428
|
+
};
|
|
429
|
+
}
|