gloomberb 0.13.1 → 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/package.json +1 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/request.ts +4 -1
- 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/index.ts +5 -1
- package/src/cli/pane-functions/screenshot.ts +2 -19
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +43 -0
- package/src/news/types.ts +1 -0
- 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 +3 -2
- package/src/plugins/builtin/cloud/askg/protocol.ts +16 -4
- 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/plugin.tsx +2 -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/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/transcript-view.tsx +8 -9
- 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/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/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +28 -18
- 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/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/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/sources/gloomberb-cloud/news.ts +1 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type { PaneFunctionCatalog } from "../../../../cli/pane-functions/catalog";
|
|
2
|
+
import { stableStringify } from "../../../../remote/revision";
|
|
3
|
+
import {
|
|
4
|
+
REMOTE_OPERATIONS,
|
|
5
|
+
REMOTE_RESOURCES,
|
|
6
|
+
remoteOperationDescriptors,
|
|
7
|
+
remoteOperationToolName,
|
|
8
|
+
} from "../../../../remote/schema";
|
|
9
|
+
import type { RemoteControlRequest } from "../../../../remote/types";
|
|
10
|
+
import type {
|
|
11
|
+
HeadlessPaneDefinition,
|
|
12
|
+
PaneDef,
|
|
13
|
+
PaneTemplateDef,
|
|
14
|
+
} from "../../../../types/plugin";
|
|
15
|
+
import {
|
|
16
|
+
TOOL_NAME_PATTERN,
|
|
17
|
+
type ClientToolManifest,
|
|
18
|
+
type ClientToolManifestSource,
|
|
19
|
+
type JsonValue,
|
|
20
|
+
type ToolManifestArgument,
|
|
21
|
+
type ToolManifestColumn,
|
|
22
|
+
type ToolManifestOption,
|
|
23
|
+
} from "./protocol";
|
|
24
|
+
|
|
25
|
+
export const HEADLESS_TOOL_TIMEOUT_MS = 30_000;
|
|
26
|
+
export const REMOTE_TOOL_TIMEOUT_MS = 10_000;
|
|
27
|
+
export const REMOTE_RESOURCE_TOOL_NAME = "app.get_resource";
|
|
28
|
+
|
|
29
|
+
const TOOL_NAME_REGEX = new RegExp(TOOL_NAME_PATTERN);
|
|
30
|
+
|
|
31
|
+
function compareText(left: string, right: string): number {
|
|
32
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SkippedToolManifest {
|
|
36
|
+
source: ClientToolManifestSource;
|
|
37
|
+
token: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ASKGClientManifest {
|
|
42
|
+
tools: ClientToolManifest[];
|
|
43
|
+
manifestHash: string;
|
|
44
|
+
skipped: SkippedToolManifest[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ManifestCandidate {
|
|
48
|
+
token: string;
|
|
49
|
+
identity: string;
|
|
50
|
+
manifest: ClientToolManifest;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type RemoteToolBinding =
|
|
54
|
+
| { kind: "operation"; operation: string }
|
|
55
|
+
| { kind: "resource" };
|
|
56
|
+
|
|
57
|
+
function projectArgument(argument: HeadlessPaneDefinition["argument"]): ToolManifestArgument {
|
|
58
|
+
return { ...argument };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function projectOptions(options: HeadlessPaneDefinition["options"]): ToolManifestOption[] {
|
|
62
|
+
return options.map(({
|
|
63
|
+
settingKey: _settingKey,
|
|
64
|
+
pluginState: _pluginState,
|
|
65
|
+
values,
|
|
66
|
+
aliases,
|
|
67
|
+
...option
|
|
68
|
+
}) => ({
|
|
69
|
+
...option,
|
|
70
|
+
...(aliases ? { aliases: [...aliases] } : {}),
|
|
71
|
+
...(values ? {
|
|
72
|
+
values: values.map((value) => ({
|
|
73
|
+
...value,
|
|
74
|
+
...(value.aliases ? { aliases: [...value.aliases] } : {}),
|
|
75
|
+
})),
|
|
76
|
+
} : {}),
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function projectColumns(columns: HeadlessPaneDefinition["columns"]): ToolManifestColumn[] | undefined {
|
|
81
|
+
if (!columns) return undefined;
|
|
82
|
+
return columns.map(({ format: _format, ...column }) => ({ ...column }));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function headlessManifest(
|
|
86
|
+
token: string,
|
|
87
|
+
title: string,
|
|
88
|
+
description: string,
|
|
89
|
+
definition: HeadlessPaneDefinition,
|
|
90
|
+
): ClientToolManifest {
|
|
91
|
+
const columns = projectColumns(definition.columns);
|
|
92
|
+
return {
|
|
93
|
+
name: token.toLowerCase(),
|
|
94
|
+
source: "headless",
|
|
95
|
+
title,
|
|
96
|
+
description,
|
|
97
|
+
writeTier: "read",
|
|
98
|
+
shape: definition.shape,
|
|
99
|
+
argument: projectArgument(definition.argument),
|
|
100
|
+
options: projectOptions(definition.options),
|
|
101
|
+
...(columns ? { columns } : {}),
|
|
102
|
+
confirm: "never",
|
|
103
|
+
timeoutMs: HEADLESS_TOOL_TIMEOUT_MS,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function definitionFor(template: PaneTemplateDef, pane: PaneDef): HeadlessPaneDefinition | undefined {
|
|
108
|
+
return template.headless ?? pane.headless;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function headlessCandidates(registry: PaneFunctionCatalog): ManifestCandidate[] {
|
|
112
|
+
const candidates: ManifestCandidate[] = [];
|
|
113
|
+
const templatedPaneIds = new Set<string>();
|
|
114
|
+
|
|
115
|
+
const templates = [...registry.paneTemplates.values()]
|
|
116
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
117
|
+
for (const template of templates) {
|
|
118
|
+
const pane = registry.panes.get(template.paneId);
|
|
119
|
+
if (!pane) continue;
|
|
120
|
+
templatedPaneIds.add(pane.id);
|
|
121
|
+
const definition = definitionFor(template, pane);
|
|
122
|
+
if (!definition) continue;
|
|
123
|
+
const token = template.shortcut?.prefix ?? template.id;
|
|
124
|
+
candidates.push({
|
|
125
|
+
token,
|
|
126
|
+
identity: `template:${template.id}`,
|
|
127
|
+
manifest: headlessManifest(token, template.label, template.description, definition),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const panes = [...registry.panes.values()]
|
|
132
|
+
.sort((left, right) => compareText(left.id, right.id));
|
|
133
|
+
for (const pane of panes) {
|
|
134
|
+
if (templatedPaneIds.has(pane.id) || !pane.headless) continue;
|
|
135
|
+
candidates.push({
|
|
136
|
+
token: pane.id,
|
|
137
|
+
identity: `pane:${pane.id}`,
|
|
138
|
+
manifest: headlessManifest(
|
|
139
|
+
pane.id,
|
|
140
|
+
pane.name,
|
|
141
|
+
`Read data from the ${pane.name} pane.`,
|
|
142
|
+
pane.headless,
|
|
143
|
+
),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return candidates;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function remoteCandidates(): ManifestCandidate[] {
|
|
151
|
+
const descriptors = remoteOperationDescriptors();
|
|
152
|
+
const operationsByName = new Map(
|
|
153
|
+
REMOTE_OPERATIONS.map((operation) => [remoteOperationToolName(operation.id), operation]),
|
|
154
|
+
);
|
|
155
|
+
const candidates = descriptors.map((descriptor): ManifestCandidate => ({
|
|
156
|
+
token: descriptor.name,
|
|
157
|
+
identity: `operation:${operationsByName.get(descriptor.name)?.id ?? descriptor.name}`,
|
|
158
|
+
manifest: {
|
|
159
|
+
name: descriptor.name,
|
|
160
|
+
source: "remote-op",
|
|
161
|
+
title: descriptor.title,
|
|
162
|
+
description: descriptor.description,
|
|
163
|
+
writeTier: descriptor.writeTier,
|
|
164
|
+
inputSchema: descriptor.inputSchema,
|
|
165
|
+
confirm: descriptor.writeTier === "user-data" || descriptor.writeTier === "broker"
|
|
166
|
+
? "always"
|
|
167
|
+
: "never",
|
|
168
|
+
timeoutMs: REMOTE_TOOL_TIMEOUT_MS,
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
candidates.push({
|
|
173
|
+
token: REMOTE_RESOURCE_TOOL_NAME,
|
|
174
|
+
identity: "resource:get",
|
|
175
|
+
manifest: {
|
|
176
|
+
name: REMOTE_RESOURCE_TOOL_NAME,
|
|
177
|
+
source: "remote-op",
|
|
178
|
+
title: "App: Get resource",
|
|
179
|
+
description: `Read a registered in-process app resource. Available resources: ${REMOTE_RESOURCES.map(({ uri }) => uri).join(", ")}.`,
|
|
180
|
+
writeTier: "read",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
resource: {
|
|
185
|
+
type: "string",
|
|
186
|
+
minLength: 1,
|
|
187
|
+
description: "Registered resource URI, such as app://snapshot or app://pane-state/{paneId}.",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ["resource"],
|
|
191
|
+
additionalProperties: false,
|
|
192
|
+
},
|
|
193
|
+
confirm: "never",
|
|
194
|
+
timeoutMs: REMOTE_TOOL_TIMEOUT_MS,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return candidates;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function compareManifests(left: ClientToolManifest, right: ClientToolManifest): number {
|
|
202
|
+
return compareText(left.name, right.name) || compareText(left.source, right.source);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function filterCandidates(candidates: ManifestCandidate[]): {
|
|
206
|
+
tools: ClientToolManifest[];
|
|
207
|
+
skipped: SkippedToolManifest[];
|
|
208
|
+
} {
|
|
209
|
+
const skipped: SkippedToolManifest[] = [];
|
|
210
|
+
const valid: ManifestCandidate[] = [];
|
|
211
|
+
|
|
212
|
+
for (const candidate of candidates.sort((left, right) => (
|
|
213
|
+
compareText(left.manifest.name, right.manifest.name)
|
|
214
|
+
|| compareText(left.identity, right.identity)
|
|
215
|
+
))) {
|
|
216
|
+
if (!TOOL_NAME_REGEX.test(candidate.manifest.name)) {
|
|
217
|
+
skipped.push({
|
|
218
|
+
source: candidate.manifest.source,
|
|
219
|
+
token: candidate.token,
|
|
220
|
+
reason: `Lowercased token "${candidate.manifest.name}" does not match ${TOOL_NAME_PATTERN}.`,
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
valid.push(candidate);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const counts = new Map<string, number>();
|
|
228
|
+
for (const candidate of valid) {
|
|
229
|
+
counts.set(candidate.manifest.name, (counts.get(candidate.manifest.name) ?? 0) + 1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tools: ClientToolManifest[] = [];
|
|
233
|
+
for (const candidate of valid) {
|
|
234
|
+
if ((counts.get(candidate.manifest.name) ?? 0) > 1) {
|
|
235
|
+
skipped.push({
|
|
236
|
+
source: candidate.manifest.source,
|
|
237
|
+
token: candidate.token,
|
|
238
|
+
reason: `Duplicate tool name "${candidate.manifest.name}" from ${candidate.identity}.`,
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
tools.push(candidate.manifest);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
tools: tools.sort(compareManifests),
|
|
247
|
+
skipped: skipped.sort((left, right) => (
|
|
248
|
+
compareText(left.token, right.token) || compareText(left.source, right.source)
|
|
249
|
+
)),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function buildASKGToolManifests(registry: PaneFunctionCatalog): {
|
|
254
|
+
tools: ClientToolManifest[];
|
|
255
|
+
skipped: SkippedToolManifest[];
|
|
256
|
+
} {
|
|
257
|
+
return filterCandidates([
|
|
258
|
+
...headlessCandidates(registry),
|
|
259
|
+
...remoteCandidates(),
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
264
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function hashASKGToolManifests(tools: readonly ClientToolManifest[]): Promise<string> {
|
|
268
|
+
const sorted = [...tools].sort(compareManifests);
|
|
269
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
270
|
+
"SHA-256",
|
|
271
|
+
new TextEncoder().encode(stableStringify(sorted)),
|
|
272
|
+
);
|
|
273
|
+
return `sha256:${bytesToHex(new Uint8Array(digest))}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function buildASKGClientManifest(
|
|
277
|
+
registry: PaneFunctionCatalog,
|
|
278
|
+
): Promise<ASKGClientManifest> {
|
|
279
|
+
const { tools, skipped } = buildASKGToolManifests(registry);
|
|
280
|
+
return {
|
|
281
|
+
tools,
|
|
282
|
+
manifestHash: await hashASKGToolManifests(tools),
|
|
283
|
+
skipped,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function resolveRemoteToolBinding(name: string): RemoteToolBinding | null {
|
|
288
|
+
if (name === REMOTE_RESOURCE_TOOL_NAME) return { kind: "resource" };
|
|
289
|
+
const operation = REMOTE_OPERATIONS.find((entry) => remoteOperationToolName(entry.id) === name);
|
|
290
|
+
return operation ? { kind: "operation", operation: operation.id } : null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resourcePatternMatches(pattern: string, value: string): boolean {
|
|
294
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
295
|
+
const regex = new RegExp(`^${escaped.replace(/\\\{[^}]+\\\}/g, "[^/]+")}$`);
|
|
296
|
+
return regex.test(value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function remoteRequestForTool(
|
|
300
|
+
binding: RemoteToolBinding,
|
|
301
|
+
args: Record<string, JsonValue>,
|
|
302
|
+
): RemoteControlRequest {
|
|
303
|
+
if (binding.kind === "operation") {
|
|
304
|
+
return { type: "call", operation: binding.operation, input: args };
|
|
305
|
+
}
|
|
306
|
+
const resource = args.resource;
|
|
307
|
+
if (typeof resource !== "string" || resource.length === 0) {
|
|
308
|
+
throw new Error(`${REMOTE_RESOURCE_TOOL_NAME} requires a resource URI.`);
|
|
309
|
+
}
|
|
310
|
+
if (!REMOTE_RESOURCES.some(({ uri }) => resourcePatternMatches(uri, resource))) {
|
|
311
|
+
throw new Error(`Unknown remote resource "${resource}".`);
|
|
312
|
+
}
|
|
313
|
+
return { type: "get", resource };
|
|
314
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ASKGSessionStartRequest,
|
|
8
8
|
type ASKGSessionStartResponse,
|
|
9
9
|
type ASKGSseEvent,
|
|
10
|
+
type ClientToolManifest,
|
|
10
11
|
type ToolManifest,
|
|
11
12
|
type ToolResultPayload,
|
|
12
13
|
} from "./protocol";
|
|
@@ -17,7 +18,7 @@ function jsonRoundTrip<T>(value: T): T {
|
|
|
17
18
|
|
|
18
19
|
function projectHeadlessDefinition(
|
|
19
20
|
definition: HeadlessPaneDefinition,
|
|
20
|
-
):
|
|
21
|
+
): ClientToolManifest {
|
|
21
22
|
const options = definition.options.map(({ settingKey: _settingKey, pluginState: _pluginState, ...option }) => option);
|
|
22
23
|
const columns = definition.columns?.map(({ format: _format, ...column }) => column);
|
|
23
24
|
return {
|
|
@@ -37,7 +38,7 @@ function projectHeadlessDefinition(
|
|
|
37
38
|
|
|
38
39
|
describe("ASKG protocol", () => {
|
|
39
40
|
test("round-trips session negotiation, a manifest, every event, and a tool result", () => {
|
|
40
|
-
const manifest:
|
|
41
|
+
const manifest: ClientToolManifest = {
|
|
41
42
|
name: "layout.place_pane",
|
|
42
43
|
source: "remote-op",
|
|
43
44
|
title: "Place pane",
|
|
@@ -19,7 +19,7 @@ export const ASKG_PROTOCOL_VERSION = 1;
|
|
|
19
19
|
export const MAX_TOOL_RESULT_BYTES = 262_144;
|
|
20
20
|
|
|
21
21
|
/** Source form for valid tool names advertised to ASKG. */
|
|
22
|
-
export const TOOL_NAME_PATTERN = "^[a-
|
|
22
|
+
export const TOOL_NAME_PATTERN = "^[a-z0-9][a-z0-9_.]{0,47}$";
|
|
23
23
|
|
|
24
24
|
/** JSON value accepted on the ASKG wire. */
|
|
25
25
|
export type JsonValue =
|
|
@@ -37,7 +37,16 @@ export type WriteTier = RemoteWriteTier;
|
|
|
37
37
|
export type ASKGClientKind = "tui" | "desktop" | "web";
|
|
38
38
|
|
|
39
39
|
/** Origin of an advertised client tool. */
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Origin of a tool. This client only ever advertises "headless" and
|
|
42
|
+
* "remote-op"; "server" describes the tools the platform runs itself and
|
|
43
|
+
* reports back in `serverTools`, so a session response can describe its whole
|
|
44
|
+
* tool surface.
|
|
45
|
+
*/
|
|
46
|
+
export type ToolManifestSource = "headless" | "remote-op" | "server";
|
|
47
|
+
|
|
48
|
+
/** The subset this client is allowed to advertise at session start. */
|
|
49
|
+
export type ClientToolManifestSource = Exclude<ToolManifestSource, "server">;
|
|
41
50
|
|
|
42
51
|
/** Headless result shapes supported by the tool timeline. */
|
|
43
52
|
export type ToolManifestShape = HeadlessPaneShape;
|
|
@@ -67,6 +76,8 @@ export type ToolInputSchemaType = RemoteJsonSchemaType;
|
|
|
67
76
|
export type ToolInputSchema = RemoteJsonSchema;
|
|
68
77
|
|
|
69
78
|
/** Client or server tool metadata negotiated when a session starts. */
|
|
79
|
+
export type ClientToolManifest = ToolManifest & { source: ClientToolManifestSource };
|
|
80
|
+
|
|
70
81
|
export interface ToolManifest {
|
|
71
82
|
/** Lowercase stable identifier matching TOOL_NAME_PATTERN. */
|
|
72
83
|
name: string;
|
|
@@ -102,7 +113,8 @@ export interface ASKGSessionStartRequest {
|
|
|
102
113
|
protocolVersion: typeof ASKG_PROTOCOL_VERSION;
|
|
103
114
|
client: ASKGClientDescriptor;
|
|
104
115
|
context: ASKGSessionContext;
|
|
105
|
-
tools
|
|
116
|
+
/** A client may only advertise tools it executes itself. */
|
|
117
|
+
tools: ClientToolManifest[];
|
|
106
118
|
manifestHash: string;
|
|
107
119
|
}
|
|
108
120
|
|
|
@@ -196,7 +208,7 @@ export interface ASKGToolExecutedEvent extends ASKGEventBase {
|
|
|
196
208
|
turnId: string;
|
|
197
209
|
toolCallId: string;
|
|
198
210
|
name: string;
|
|
199
|
-
source: ToolManifestSource
|
|
211
|
+
source: ToolManifestSource;
|
|
200
212
|
status: ToolResultStatus;
|
|
201
213
|
summary: ToolExecutionSummary;
|
|
202
214
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { applyJsonPatch } from "../../../../remote/json-patch";
|
|
3
|
+
import { revisionFor } from "../../../../remote/revision";
|
|
4
|
+
import type { RemoteControlResponse } from "../../../../remote/types";
|
|
5
|
+
import type { JsonValue } from "./protocol";
|
|
6
|
+
import { ASKGUndoManager, type InProcessRemoteControlHandler } from "./undo";
|
|
7
|
+
|
|
8
|
+
test("restores reversible pane state only when it has not changed again", async () => {
|
|
9
|
+
const resource = "app://pane-state/chart%3Amain";
|
|
10
|
+
const values = new Map<string, JsonValue>([[resource, { cursor: 1, mode: "price" }]]);
|
|
11
|
+
const handler: InProcessRemoteControlHandler = async (request): Promise<RemoteControlResponse> => {
|
|
12
|
+
if (request.type === "get") {
|
|
13
|
+
const value = values.get(request.resource) ?? {};
|
|
14
|
+
return { ok: true, data: value, rev: revisionFor(value) };
|
|
15
|
+
}
|
|
16
|
+
if (request.type === "patch") {
|
|
17
|
+
const current = values.get(request.resource) ?? {};
|
|
18
|
+
if (request.expectRev && request.expectRev !== revisionFor(current)) {
|
|
19
|
+
return { ok: false, error: { code: "revision_mismatch", message: "Revision mismatch." } };
|
|
20
|
+
}
|
|
21
|
+
const next = applyJsonPatch(current, request.patch) as JsonValue;
|
|
22
|
+
values.set(request.resource, next);
|
|
23
|
+
return { ok: true, data: next, rev: revisionFor(next) };
|
|
24
|
+
}
|
|
25
|
+
return { ok: false, error: { code: "unsupported", message: "Unsupported request." } };
|
|
26
|
+
};
|
|
27
|
+
const undo = new ASKGUndoManager(handler, { tokenFactory: () => "undo-1" });
|
|
28
|
+
const prepared = await undo.prepare("pane.setState", { paneId: "chart:main", patch: { cursor: 2 } });
|
|
29
|
+
expect(prepared).not.toBeNull();
|
|
30
|
+
values.set(resource, { cursor: 2, mode: "price", transient: true });
|
|
31
|
+
const token = await undo.commit(prepared!);
|
|
32
|
+
|
|
33
|
+
expect(token).toBe("undo-1");
|
|
34
|
+
expect((await undo.undo(token!)).status).toBe("ok");
|
|
35
|
+
expect(values.get(resource)).toEqual({ cursor: 1, mode: "price" });
|
|
36
|
+
|
|
37
|
+
const second = await undo.prepare("pane.setState", { paneId: "chart:main", patch: { cursor: 3 } });
|
|
38
|
+
values.set(resource, { cursor: 3, mode: "price" });
|
|
39
|
+
const secondToken = await undo.commit(second!);
|
|
40
|
+
values.set(resource, { cursor: 4, mode: "price" });
|
|
41
|
+
const denied = await undo.undo(secondToken!);
|
|
42
|
+
expect(denied.status).toBe("denied");
|
|
43
|
+
expect(denied.note).toContain("changed after the tool call");
|
|
44
|
+
});
|