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.
Files changed (219) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -2
  3. package/src/api-client/auth.ts +2 -1
  4. package/src/api-client/index.test.ts +34 -0
  5. package/src/api-client/index.ts +10 -2
  6. package/src/api-client/request.ts +4 -1
  7. package/src/api-client/research-activity.ts +61 -0
  8. package/src/api-client/types.ts +2 -1
  9. package/src/cli/commands/plugins.ts +4 -3
  10. package/src/cli/desktop-pane-shot.ts +79 -10
  11. package/src/cli/pane-functions/capabilities.ts +76 -23
  12. package/src/cli/pane-functions/cloud-session.test.ts +54 -0
  13. package/src/cli/pane-functions/cloud-session.ts +38 -0
  14. package/src/cli/pane-functions/dom.test.ts +41 -0
  15. package/src/cli/pane-functions/dom.ts +199 -0
  16. package/src/cli/pane-functions/index.test.ts +13 -2
  17. package/src/cli/pane-functions/index.ts +9 -5
  18. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  19. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  20. package/src/cli/pane-functions/report.test.ts +29 -1
  21. package/src/cli/pane-functions/report.ts +68 -7
  22. package/src/cli/pane-functions/screenshot.ts +195 -42
  23. package/src/cli/restore-plugins.ts +10 -3
  24. package/src/components/chart/composite/composite-chart.test.tsx +68 -1
  25. package/src/components/chart/composite/composite-chart.tsx +87 -21
  26. package/src/components/chart/composite/rasterizer.ts +33 -0
  27. package/src/components/chart/composite/renderers.test.ts +51 -4
  28. package/src/components/chart/composite/scene.test.ts +128 -2
  29. package/src/components/chart/composite/scene.ts +92 -16
  30. package/src/components/chart/composite/text-renderer.ts +13 -0
  31. package/src/components/chart/composite/time-scale.ts +30 -10
  32. package/src/components/chart/composite/types.ts +26 -0
  33. package/src/components/command-bar/assist/runtime.ts +38 -30
  34. package/src/components/command-bar/surface/assist.test.tsx +63 -9
  35. package/src/components/command-bar/surface/test-harness.tsx +23 -4
  36. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  37. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  38. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  39. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  40. package/src/data/config/store/index.test.ts +23 -0
  41. package/src/data/config/store/node.ts +7 -1
  42. package/src/data/config/store/normalize.ts +4 -0
  43. package/src/news/types.ts +1 -0
  44. package/src/plugins/builtin/cds/client.test.ts +20 -0
  45. package/src/plugins/builtin/cds/client.ts +13 -3
  46. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  47. package/src/plugins/builtin/cds/headless.ts +113 -0
  48. package/src/plugins/builtin/cds/index.tsx +4 -0
  49. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  50. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  51. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  52. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  53. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  54. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  55. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  56. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  57. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  58. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  59. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  60. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  61. package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
  62. package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
  63. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  64. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  65. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  66. package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
  67. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  68. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  69. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  70. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  71. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  72. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  73. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  74. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  75. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  76. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  77. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  78. package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
  79. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  80. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  81. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  82. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  83. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  84. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  85. package/src/plugins/builtin/earnings/headless.ts +162 -0
  86. package/src/plugins/builtin/earnings/index.tsx +2 -0
  87. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  88. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  89. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  90. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  91. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
  93. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
  94. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  95. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  96. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  97. package/src/plugins/builtin/holders/client.ts +29 -0
  98. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  99. package/src/plugins/builtin/holders/headless.ts +162 -0
  100. package/src/plugins/builtin/holders/index.tsx +14 -8
  101. package/src/plugins/builtin/holders/pane.tsx +7 -1
  102. package/src/plugins/builtin/insider/client.ts +50 -0
  103. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  104. package/src/plugins/builtin/insider/headless.ts +117 -0
  105. package/src/plugins/builtin/insider/index.tsx +21 -50
  106. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  107. package/src/plugins/builtin/insider/model.ts +72 -0
  108. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  109. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  110. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  111. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  112. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  113. package/src/plugins/builtin/market-movers/client.ts +76 -0
  114. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  115. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  116. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  117. package/src/plugins/builtin/news/headless.test.ts +138 -0
  118. package/src/plugins/builtin/news/headless.ts +190 -0
  119. package/src/plugins/builtin/news/index.tsx +13 -9
  120. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  121. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  122. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  123. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  124. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  125. package/src/plugins/builtin/research/client.ts +12 -0
  126. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  127. package/src/plugins/builtin/research/event-model.ts +245 -0
  128. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  129. package/src/plugins/builtin/research/events-headless.ts +87 -0
  130. package/src/plugins/builtin/research/headless.test.ts +93 -0
  131. package/src/plugins/builtin/research/headless.ts +161 -0
  132. package/src/plugins/builtin/research/index.tsx +32 -18
  133. package/src/plugins/builtin/research-search/pane.tsx +4 -0
  134. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  135. package/src/plugins/builtin/scanner/headless.ts +125 -0
  136. package/src/plugins/builtin/scanner/index.ts +4 -0
  137. package/src/plugins/builtin/sec/client.ts +16 -0
  138. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  139. package/src/plugins/builtin/sec/headless.ts +69 -0
  140. package/src/plugins/builtin/sec/index.tsx +20 -75
  141. package/src/plugins/builtin/sec/model.ts +89 -0
  142. package/src/plugins/builtin/sectors/client.ts +54 -0
  143. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  144. package/src/plugins/builtin/sectors/headless.ts +80 -0
  145. package/src/plugins/builtin/sectors/index.tsx +6 -51
  146. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  147. package/src/plugins/builtin/short-interest/client.ts +5 -2
  148. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  149. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  150. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  151. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  152. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  153. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  154. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  155. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  156. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  157. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  158. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  159. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  160. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  161. package/src/plugins/builtin/volatility/client.ts +16 -3
  162. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  163. package/src/plugins/builtin/volatility/headless.ts +75 -0
  164. package/src/plugins/builtin/volatility/index.tsx +4 -0
  165. package/src/plugins/builtin/world-indices/client.ts +43 -0
  166. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  167. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  168. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  169. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  170. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  171. package/src/plugins/builtin/yield-curve/index.tsx +6 -8
  172. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  173. package/src/plugins/bundle.test.ts +58 -1
  174. package/src/plugins/bundle.ts +4 -2
  175. package/src/plugins/catalog-backend.test.ts +26 -0
  176. package/src/plugins/catalog-backend.ts +9 -2
  177. package/src/plugins/catalog-browser.test.ts +0 -13
  178. package/src/plugins/catalog-browser.ts +4 -0
  179. package/src/plugins/catalog-manifest.test.ts +15 -0
  180. package/src/plugins/catalog-manifest.ts +12 -5
  181. package/src/plugins/catalog-ui.ts +0 -6
  182. package/src/plugins/host-contract.ts +9 -0
  183. package/src/plugins/host-modules.ts +12 -0
  184. package/src/plugins/loader.ts +63 -15
  185. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  186. package/src/plugins/prediction-markets/headless.ts +255 -0
  187. package/src/plugins/prediction-markets/index.tsx +2 -0
  188. package/src/plugins/seed.ts +3 -0
  189. package/src/public/broker.ts +6 -0
  190. package/src/remote/schema.test.ts +65 -0
  191. package/src/remote/schema.ts +322 -39
  192. package/src/remote/types.ts +49 -2
  193. package/src/renderers/browser/app-services.ts +3 -0
  194. package/src/renderers/browser/config-host.ts +24 -8
  195. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  196. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  197. package/src/renderers/browser/main.tsx +3 -1
  198. package/src/renderers/browser/research-entry.test.ts +8 -0
  199. package/src/renderers/browser/research-entry.ts +7 -0
  200. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  201. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  202. package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
  203. package/src/renderers/electrobun/view/build-assets.ts +0 -2
  204. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
  205. package/src/sources/gloomberb-cloud/news.ts +1 -0
  206. package/src/sync/core-contributors.test.ts +111 -0
  207. package/src/sync/core-contributors.ts +155 -1
  208. package/src/time-series/resolve.test.ts +118 -1
  209. package/src/time-series/resolve.ts +14 -16
  210. package/src/types/config.ts +2 -0
  211. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  212. package/src/plugins/broker-sync/native-loader.ts +0 -10
  213. package/src/plugins/broker-sync/normalize.ts +0 -308
  214. package/src/plugins/broker-sync/public.ts +0 -122
  215. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  216. package/src/plugins/broker-sync/robinhood.ts +0 -114
  217. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  218. package/src/plugins/broker-sync/simplefin.ts +0 -70
  219. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
@@ -0,0 +1,286 @@
1
+ // Wire declarations are mirrored in gloomberb-platform/server/src/services/askg/types.ts.
2
+ import type {
3
+ HeadlessPaneArgumentDef,
4
+ HeadlessPaneColumn,
5
+ HeadlessPaneOptionDef,
6
+ HeadlessPaneOptionValue,
7
+ HeadlessPaneShape,
8
+ } from "../../../../types/headless";
9
+ import type {
10
+ RemoteJsonSchema,
11
+ RemoteJsonSchemaType,
12
+ RemoteWriteTier,
13
+ } from "../../../../remote/types";
14
+
15
+ /** Major version of the ASKG client and server wire contract. */
16
+ export const ASKG_PROTOCOL_VERSION = 1;
17
+
18
+ /** Maximum accepted JSON-encoded client tool result size. */
19
+ export const MAX_TOOL_RESULT_BYTES = 262_144;
20
+
21
+ /** Source form for valid tool names advertised to ASKG. */
22
+ export const TOOL_NAME_PATTERN = "^[a-z0-9][a-z0-9_.]{0,47}$";
23
+
24
+ /** JSON value accepted on the ASKG wire. */
25
+ export type JsonValue =
26
+ | string
27
+ | number
28
+ | boolean
29
+ | null
30
+ | JsonValue[]
31
+ | { [key: string]: JsonValue };
32
+
33
+ /** Safety tier applied before a client tool is executed. */
34
+ export type WriteTier = RemoteWriteTier;
35
+
36
+ /** Runtime that opened an ASKG session. */
37
+ export type ASKGClientKind = "tui" | "desktop" | "web";
38
+
39
+ /** Origin of an advertised client tool. */
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">;
50
+
51
+ /** Headless result shapes supported by the tool timeline. */
52
+ export type ToolManifestShape = HeadlessPaneShape;
53
+
54
+ /** Headless argument kinds understood by the manifest compiler. */
55
+ export type ToolManifestArgumentKind = HeadlessPaneArgumentDef["kind"];
56
+
57
+ /** Serializable headless argument declaration. */
58
+ export type ToolManifestArgument = HeadlessPaneArgumentDef;
59
+
60
+ /** Values accepted by a headless enum option. */
61
+ export type ToolManifestOptionValue = HeadlessPaneOptionValue;
62
+
63
+ /** Headless option types understood by the manifest compiler. */
64
+ export type ToolManifestOptionType = HeadlessPaneOptionDef["type"];
65
+
66
+ /** Serializable headless option declaration. */
67
+ export type ToolManifestOption = Omit<HeadlessPaneOptionDef, "settingKey" | "pluginState">;
68
+
69
+ /** Serializable headless column declaration. */
70
+ export type ToolManifestColumn = Omit<HeadlessPaneColumn, "format">;
71
+
72
+ /** JSON Schema primitive types accepted for remote operation inputs. */
73
+ export type ToolInputSchemaType = RemoteJsonSchemaType;
74
+
75
+ /** JSON Schema accepted by a remote operation tool. */
76
+ export type ToolInputSchema = RemoteJsonSchema;
77
+
78
+ /** Client or server tool metadata negotiated when a session starts. */
79
+ export type ClientToolManifest = ToolManifest & { source: ClientToolManifestSource };
80
+
81
+ export interface ToolManifest {
82
+ /** Lowercase stable identifier matching TOOL_NAME_PATTERN. */
83
+ name: string;
84
+ source: ToolManifestSource;
85
+ title: string;
86
+ description: string;
87
+ writeTier: WriteTier;
88
+ shape?: ToolManifestShape;
89
+ argument?: ToolManifestArgument;
90
+ options?: ToolManifestOption[];
91
+ columns?: ToolManifestColumn[];
92
+ inputSchema?: ToolInputSchema;
93
+ confirm: "never" | "always";
94
+ timeoutMs: number;
95
+ }
96
+
97
+ /** Client identity included in a session negotiation. */
98
+ export interface ASKGClientDescriptor {
99
+ kind: ASKGClientKind;
100
+ version: string;
101
+ }
102
+
103
+ /** Terminal context supplied to the model at session start. */
104
+ export interface ASKGSessionContext {
105
+ query?: string;
106
+ symbol?: string;
107
+ paneId?: string;
108
+ layout?: JsonValue;
109
+ }
110
+
111
+ /** Request used to negotiate tools and limits for an ASKG session. */
112
+ export interface ASKGSessionStartRequest {
113
+ protocolVersion: typeof ASKG_PROTOCOL_VERSION;
114
+ client: ASKGClientDescriptor;
115
+ context: ASKGSessionContext;
116
+ /** A client may only advertise tools it executes itself. */
117
+ tools: ClientToolManifest[];
118
+ manifestHash: string;
119
+ }
120
+
121
+ /** Per-user and per-turn limits returned by the platform. */
122
+ export interface ASKGLimits {
123
+ requestsPerMinute: number;
124
+ turnsPerDay: number;
125
+ turnsRemainingToday: number;
126
+ maxToolCallsPerTurn: number;
127
+ turnWallClockMs: number;
128
+ clientToolTimeoutMs: number;
129
+ }
130
+
131
+ /** Client tool rejected during session negotiation. */
132
+ export interface ASKGRejectedTool {
133
+ name: string;
134
+ reason: string;
135
+ }
136
+
137
+ /** Successful ASKG session negotiation response. */
138
+ export interface ASKGSessionStartResponse {
139
+ protocolVersion: typeof ASKG_PROTOCOL_VERSION;
140
+ sessionId: string;
141
+ manifestHash: string;
142
+ serverTools: ToolManifest[];
143
+ acceptedTools: string[];
144
+ rejectedTools: ASKGRejectedTool[];
145
+ limits: ASKGLimits;
146
+ tier: string;
147
+ model: string;
148
+ promptVersion: string;
149
+ expiresAt: string;
150
+ }
151
+
152
+ /** Shared fields carried by every turn stream event. */
153
+ export interface ASKGEventBase {
154
+ seq: number;
155
+ }
156
+
157
+ /** Announces the session and turn attached to this stream. */
158
+ export interface ASKGSessionEvent extends ASKGEventBase {
159
+ type: "session";
160
+ sessionId: string;
161
+ turnId: string;
162
+ model: string;
163
+ promptVersion: string;
164
+ }
165
+
166
+ /** Appends model text to the visible answer. */
167
+ export interface ASKGTextDeltaEvent extends ASKGEventBase {
168
+ type: "text-delta";
169
+ turnId: string;
170
+ delta: string;
171
+ }
172
+
173
+ /** Delegates one negotiated tool call to the client. */
174
+ export interface ASKGToolCallEvent extends ASKGEventBase {
175
+ type: "tool-call";
176
+ turnId: string;
177
+ toolCallId: string;
178
+ name: string;
179
+ args: Record<string, JsonValue>;
180
+ writeTier: WriteTier;
181
+ requiresConfirmation: boolean;
182
+ preview: JsonValue;
183
+ timeoutMs: number;
184
+ expiresAt: string;
185
+ }
186
+
187
+ /** Compact result information suitable for the tool timeline. */
188
+ export interface ToolExecutionSummary {
189
+ rowCount?: number;
190
+ elapsedMs: number;
191
+ truncated: boolean;
192
+ note?: string;
193
+ sample?: JsonValue;
194
+ }
195
+
196
+ /** Status returned by either a server or client tool execution. */
197
+ export type ToolResultStatus =
198
+ | "ok"
199
+ | "error"
200
+ | "denied"
201
+ | "timeout"
202
+ | "partial"
203
+ | "cancelled";
204
+
205
+ /** Reports a completed server tool or an accepted client execution. */
206
+ export interface ASKGToolExecutedEvent extends ASKGEventBase {
207
+ type: "tool-executed";
208
+ turnId: string;
209
+ toolCallId: string;
210
+ name: string;
211
+ source: ToolManifestSource;
212
+ status: ToolResultStatus;
213
+ summary: ToolExecutionSummary;
214
+ }
215
+
216
+ /** Confirms that one client tool result was accepted by the turn loop. */
217
+ export interface ASKGToolResultAckEvent extends ASKGEventBase {
218
+ type: "tool-result-ack";
219
+ turnId: string;
220
+ toolCallId: string;
221
+ }
222
+
223
+ /** Stable error codes that clients can handle without parsing text. */
224
+ export type ASKGErrorCode =
225
+ | "rate_limited"
226
+ | "daily_turn_cap"
227
+ | "tool_budget_exhausted"
228
+ | "turn_timeout"
229
+ | "model_usage_limit"
230
+ | "model_unavailable"
231
+ | "internal";
232
+
233
+ /** Reports a recoverable or terminal session or turn failure. */
234
+ export interface ASKGErrorEvent extends ASKGEventBase {
235
+ type: "error";
236
+ turnId?: string;
237
+ code: ASKGErrorCode;
238
+ message: string;
239
+ retryable: boolean;
240
+ retryAfterMs?: number;
241
+ }
242
+
243
+ /** Token accounting emitted when the model provider supplies it. */
244
+ export interface ASKGUsageEvent extends ASKGEventBase {
245
+ type: "usage";
246
+ turnId: string;
247
+ inputTokens?: number;
248
+ outputTokens?: number;
249
+ totalTokens?: number;
250
+ estimated: boolean;
251
+ }
252
+
253
+ /** Reason a turn stream stopped. */
254
+ export type ASKGDoneReason = "complete" | "cancelled" | "error" | "timeout";
255
+
256
+ /** Terminates a turn stream. */
257
+ export interface ASKGDoneEvent extends ASKGEventBase {
258
+ type: "done";
259
+ turnId: string;
260
+ reason: ASKGDoneReason;
261
+ }
262
+
263
+ /** Event payloads emitted over the ASKG turn SSE stream. */
264
+ export type ASKGSseEvent =
265
+ | ASKGSessionEvent
266
+ | ASKGTextDeltaEvent
267
+ | ASKGToolCallEvent
268
+ | ASKGToolExecutedEvent
269
+ | ASKGToolResultAckEvent
270
+ | ASKGErrorEvent
271
+ | ASKGUsageEvent
272
+ | ASKGDoneEvent;
273
+
274
+ /** Client result posted for one delegated tool call. */
275
+ export interface ToolResultPayload {
276
+ turnId: string;
277
+ toolCallId: string;
278
+ status: ToolResultStatus;
279
+ result?: JsonValue;
280
+ rowCount?: number;
281
+ truncated: boolean;
282
+ elapsedMs: number;
283
+ note?: string;
284
+ rev?: string;
285
+ undoToken?: string;
286
+ }
@@ -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
+ });
@@ -0,0 +1,277 @@
1
+ import { stableStringify } from "../../../../remote/revision";
2
+ import type {
3
+ RemoteControlRequest,
4
+ RemoteControlResponse,
5
+ RemoteJsonPatchOperation,
6
+ } from "../../../../remote/types";
7
+ import type { JsonValue, ToolResultStatus } from "./protocol";
8
+
9
+ export type InProcessRemoteControlHandler = (
10
+ request: RemoteControlRequest,
11
+ ) => Promise<RemoteControlResponse>;
12
+
13
+ interface UndoResourceSnapshot {
14
+ resource: string;
15
+ value: JsonValue;
16
+ }
17
+
18
+ interface StoredUndoResource extends UndoResourceSnapshot {
19
+ expectedRev: string;
20
+ }
21
+
22
+ interface StoredUndo {
23
+ createdAt: number;
24
+ resources: StoredUndoResource[];
25
+ }
26
+
27
+ export interface PreparedUndo {
28
+ resources: UndoResourceSnapshot[];
29
+ }
30
+
31
+ export interface AppliedUndo {
32
+ status: Extract<ToolResultStatus, "ok" | "error" | "denied" | "cancelled">;
33
+ elapsedMs: number;
34
+ note?: string;
35
+ }
36
+
37
+ export interface ASKGUndoManagerOptions {
38
+ maxEntries?: number;
39
+ ttlMs?: number;
40
+ now?: () => number;
41
+ tokenFactory?: () => string;
42
+ }
43
+
44
+ const DEFAULT_MAX_ENTRIES = 50;
45
+ const DEFAULT_TTL_MS = 10 * 60_000;
46
+
47
+ const LAYOUT_UNDO_OPERATIONS = new Set([
48
+ "pane.setSetting",
49
+ "layout.gridlock",
50
+ "layout.closeFloating",
51
+ "layout.placePane",
52
+ "layout.setGrid",
53
+ ]);
54
+
55
+ function shortReason(error: unknown): string {
56
+ const text = error instanceof Error ? error.message : String(error);
57
+ return text.replace(/\s+/g, " ").trim().slice(0, 240) || "Unknown error.";
58
+ }
59
+
60
+ function normalizeJson(value: unknown, seen = new WeakSet<object>()): JsonValue {
61
+ if (value == null) return null;
62
+ if (typeof value === "string" || typeof value === "boolean") return value;
63
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
64
+ if (typeof value === "bigint") return value.toString();
65
+ if (value instanceof Date) return value.toISOString();
66
+ if (typeof value !== "object") return String(value);
67
+ if (seen.has(value)) return "[Circular]";
68
+ seen.add(value);
69
+ if (Array.isArray(value)) {
70
+ const result = value.map((entry) => normalizeJson(entry, seen));
71
+ seen.delete(value);
72
+ return result;
73
+ }
74
+ const result: Record<string, JsonValue> = {};
75
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
76
+ result[key] = normalizeJson((value as Record<string, unknown>)[key], seen);
77
+ }
78
+ seen.delete(value);
79
+ return result;
80
+ }
81
+
82
+ function paneStateResource(args: Record<string, JsonValue>): string | null {
83
+ const paneId = args.paneId;
84
+ return typeof paneId === "string" && paneId.length > 0
85
+ ? `app://pane-state/${encodeURIComponent(paneId)}`
86
+ : null;
87
+ }
88
+
89
+ function resourcesForOperation(
90
+ operation: string,
91
+ args: Record<string, JsonValue>,
92
+ ): string[] | null {
93
+ if (operation === "pane.setState") {
94
+ const resource = paneStateResource(args);
95
+ return resource ? [resource] : null;
96
+ }
97
+ if (LAYOUT_UNDO_OPERATIONS.has(operation)) return ["app://layout/current"];
98
+ return null;
99
+ }
100
+
101
+ function pointerSegment(value: string): string {
102
+ return value.replace(/~/g, "~0").replace(/\//g, "~1");
103
+ }
104
+
105
+ function isObject(value: JsonValue): value is Record<string, JsonValue> {
106
+ return value != null && typeof value === "object" && !Array.isArray(value);
107
+ }
108
+
109
+ function replacementPatch(current: JsonValue, target: JsonValue): RemoteJsonPatchOperation[] | null {
110
+ if (!isObject(current) || !isObject(target)) return null;
111
+ const patch: RemoteJsonPatchOperation[] = [];
112
+ const currentKeys = Object.keys(current).sort();
113
+ const targetKeys = Object.keys(target).sort();
114
+ for (const key of currentKeys) {
115
+ if (!(key in target)) patch.push({ op: "remove", path: `/${pointerSegment(key)}` });
116
+ }
117
+ for (const key of targetKeys) {
118
+ const path = `/${pointerSegment(key)}`;
119
+ if (!(key in current)) {
120
+ patch.push({ op: "add", path, value: target[key] });
121
+ } else if (stableStringify(current[key]) !== stableStringify(target[key])) {
122
+ patch.push({ op: "replace", path, value: target[key] });
123
+ }
124
+ }
125
+ return patch;
126
+ }
127
+
128
+ function throwIfCancelled(signal?: AbortSignal): void {
129
+ if (!signal?.aborted) return;
130
+ throw signal.reason instanceof Error ? signal.reason : new Error("Undo was cancelled.");
131
+ }
132
+
133
+ function randomToken(): string {
134
+ return globalThis.crypto.randomUUID();
135
+ }
136
+
137
+ export class ASKGUndoManager {
138
+ private readonly entries = new Map<string, StoredUndo>();
139
+ private readonly maxEntries: number;
140
+ private readonly ttlMs: number;
141
+ private readonly now: () => number;
142
+ private readonly tokenFactory: () => string;
143
+
144
+ constructor(
145
+ private readonly remoteHandler: InProcessRemoteControlHandler,
146
+ options: ASKGUndoManagerOptions = {},
147
+ ) {
148
+ this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
149
+ this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
150
+ this.now = options.now ?? Date.now;
151
+ this.tokenFactory = options.tokenFactory ?? randomToken;
152
+ }
153
+
154
+ async prepare(
155
+ operation: string,
156
+ args: Record<string, JsonValue>,
157
+ signal?: AbortSignal,
158
+ ): Promise<PreparedUndo | null> {
159
+ const resources = resourcesForOperation(operation, args);
160
+ if (!resources) return null;
161
+ const snapshots: UndoResourceSnapshot[] = [];
162
+ for (const resource of resources) {
163
+ throwIfCancelled(signal);
164
+ const response = await this.remoteHandler({ type: "get", resource });
165
+ if (!response.ok) throw new Error(response.error.message);
166
+ snapshots.push({ resource, value: normalizeJson(response.data) });
167
+ }
168
+ return { resources: snapshots };
169
+ }
170
+
171
+ async commit(prepared: PreparedUndo, signal?: AbortSignal): Promise<string | null> {
172
+ const resources: StoredUndoResource[] = [];
173
+ let changed = false;
174
+ for (const before of prepared.resources) {
175
+ throwIfCancelled(signal);
176
+ const response = await this.remoteHandler({ type: "get", resource: before.resource });
177
+ if (!response.ok) throw new Error(response.error.message);
178
+ if (!response.rev) throw new Error(`Resource "${before.resource}" did not return a revision.`);
179
+ const current = normalizeJson(response.data);
180
+ if (stableStringify(current) !== stableStringify(before.value)) changed = true;
181
+ resources.push({ ...before, expectedRev: response.rev });
182
+ }
183
+ if (!changed) return null;
184
+
185
+ this.prune();
186
+ const token = this.tokenFactory();
187
+ this.entries.set(token, { createdAt: this.now(), resources });
188
+ while (this.entries.size > this.maxEntries) {
189
+ const oldest = this.entries.keys().next().value as string | undefined;
190
+ if (!oldest) break;
191
+ this.entries.delete(oldest);
192
+ }
193
+ return token;
194
+ }
195
+
196
+ async undo(token: string, signal?: AbortSignal): Promise<AppliedUndo> {
197
+ const startedAt = this.now();
198
+ this.prune();
199
+ const entry = this.entries.get(token);
200
+ if (!entry) {
201
+ return {
202
+ status: "denied",
203
+ elapsedMs: Math.max(0, this.now() - startedAt),
204
+ note: "Undo token is unknown or expired.",
205
+ };
206
+ }
207
+
208
+ try {
209
+ const currentResources: Array<{
210
+ snapshot: StoredUndoResource;
211
+ value: JsonValue;
212
+ rev: string;
213
+ patch: RemoteJsonPatchOperation[];
214
+ }> = [];
215
+ for (const snapshot of entry.resources) {
216
+ throwIfCancelled(signal);
217
+ const response = await this.remoteHandler({ type: "get", resource: snapshot.resource });
218
+ if (!response.ok) throw new Error(response.error.message);
219
+ if (!response.rev) throw new Error(`Resource "${snapshot.resource}" did not return a revision.`);
220
+ if (response.rev !== snapshot.expectedRev) {
221
+ return {
222
+ status: "denied",
223
+ elapsedMs: Math.max(0, this.now() - startedAt),
224
+ note: `Cannot undo because "${snapshot.resource}" changed after the tool call.`,
225
+ };
226
+ }
227
+ const value = normalizeJson(response.data);
228
+ const patch = replacementPatch(value, snapshot.value);
229
+ if (!patch) {
230
+ return {
231
+ status: "denied",
232
+ elapsedMs: Math.max(0, this.now() - startedAt),
233
+ note: `Cannot safely restore non-object resource "${snapshot.resource}".`,
234
+ };
235
+ }
236
+ currentResources.push({ snapshot, value, rev: response.rev, patch });
237
+ }
238
+
239
+ for (const current of currentResources) {
240
+ throwIfCancelled(signal);
241
+ if (current.patch.length === 0) continue;
242
+ const response = await this.remoteHandler({
243
+ type: "patch",
244
+ resource: current.snapshot.resource,
245
+ patch: current.patch,
246
+ expectRev: current.rev,
247
+ });
248
+ if (!response.ok) throw new Error(response.error.message);
249
+ }
250
+ this.entries.delete(token);
251
+ return {
252
+ status: "ok",
253
+ elapsedMs: Math.max(0, this.now() - startedAt),
254
+ };
255
+ } catch (error) {
256
+ if (signal?.aborted) {
257
+ return {
258
+ status: "cancelled",
259
+ elapsedMs: Math.max(0, this.now() - startedAt),
260
+ note: "Undo was cancelled.",
261
+ };
262
+ }
263
+ return {
264
+ status: "error",
265
+ elapsedMs: Math.max(0, this.now() - startedAt),
266
+ note: shortReason(error),
267
+ };
268
+ }
269
+ }
270
+
271
+ private prune(): void {
272
+ const cutoff = this.now() - this.ttlMs;
273
+ for (const [token, entry] of this.entries) {
274
+ if (entry.createdAt < cutoff) this.entries.delete(token);
275
+ }
276
+ }
277
+ }