@robota-sdk/agent-transport 3.0.0-beta.83 → 3.0.0-beta.84
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/CHANGELOG.md +43 -0
- package/README.md +80 -118
- package/dist/browser/client.d.ts +1 -1
- package/dist/browser/client.js +1 -1
- package/dist/browser/index.d.ts +60 -6
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/observer-messages-CEeisRaF.js +2 -0
- package/dist/browser/observer-messages-CEeisRaF.js.map +1 -0
- package/dist/browser/{observer-messages-DC_8VHk9.d.ts → observer-messages-Dnff2Tdb.d.ts} +125 -3
- package/dist/browser/observer-messages-Dnff2Tdb.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +181 -6
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +181 -6
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/package.json +12 -11
- package/src/__tests__/message-decoders.test.ts +295 -1
- package/src/__tests__/project-read-messages.test.ts +118 -0
- package/src/__tests__/session-directory-messages.test.ts +130 -0
- package/src/__tests__/session-error-classification.test.ts +113 -0
- package/src/__tests__/session-message-handler-observe.test.ts +25 -1
- package/src/__tests__/session-message-handler.test.ts +57 -3
- package/src/__tests__/settings-messages.test.ts +193 -0
- package/src/index.ts +4 -0
- package/src/message-decoders.ts +157 -3
- package/src/observer-messages.ts +11 -0
- package/src/project-read-messages.ts +74 -0
- package/src/protocol-session.ts +17 -1
- package/src/session-directory-messages.ts +78 -1
- package/src/session-error-classification.ts +91 -0
- package/src/session-events.ts +7 -1
- package/src/session-message-handler.ts +58 -8
- package/src/session-query-messages.ts +18 -1
- package/src/session-resume-bridge.ts +8 -0
- package/src/settings-messages.ts +103 -0
- package/src/wire-messages.ts +133 -4
- package/dist/browser/observer-messages-Bx-xReUQ.js +0 -2
- package/dist/browser/observer-messages-Bx-xReUQ.js.map +0 -1
- package/dist/browser/observer-messages-DC_8VHk9.d.ts.map +0 -1
|
@@ -15,9 +15,14 @@ import {
|
|
|
15
15
|
import { handleLoopControlMessage, isLoopControlMessage } from './loop-control-messages.js';
|
|
16
16
|
import { parseClientMessage } from './message-parser.js';
|
|
17
17
|
import { isObserverMessageType } from './observer-messages.js';
|
|
18
|
+
import { handleProjectReadMessage, isProjectReadMessage } from './project-read-messages.js';
|
|
18
19
|
import {
|
|
20
|
+
handleSessionDeleteMessage,
|
|
19
21
|
handleSessionDirectoryMessage,
|
|
22
|
+
handleSessionRenameMessage,
|
|
23
|
+
isSessionDeleteMessage,
|
|
20
24
|
isSessionDirectoryMessage,
|
|
25
|
+
isSessionRenameMessage,
|
|
21
26
|
} from './session-directory-messages.js';
|
|
22
27
|
import { subscribeSessionEvents } from './session-events.js';
|
|
23
28
|
import {
|
|
@@ -25,12 +30,15 @@ import {
|
|
|
25
30
|
isSessionQueryMessage,
|
|
26
31
|
pendingFrame,
|
|
27
32
|
} from './session-query-messages.js';
|
|
33
|
+
import { handleSettingsMessage, isSettingsMessage } from './settings-messages.js';
|
|
28
34
|
import { handleUsageQueryMessage } from './usage-messages.js';
|
|
29
35
|
|
|
30
36
|
import type { TOutboundDeliver } from './outbound-delivery.js';
|
|
31
|
-
import type { IProtocolSession } from './protocol-session.js';
|
|
37
|
+
import type { IProtocolSession, TProjectReadCapableSession } from './protocol-session.js';
|
|
38
|
+
import type { ISettingsReporter } from './settings-messages.js';
|
|
32
39
|
import type { IUsageQueryReporters } from './usage-messages.js';
|
|
33
40
|
import type { TClientMessage } from './wire-messages.js';
|
|
41
|
+
import type { TCommandSurfaceLocality } from '@robota-sdk/agent-interface-command';
|
|
34
42
|
import type { TUsageSurface } from '@robota-sdk/agent-interface-analytics';
|
|
35
43
|
import type { ISessionDirectory, TDriverId } from '@robota-sdk/agent-interface-session';
|
|
36
44
|
|
|
@@ -48,8 +56,8 @@ export { parseClientMessage } from './message-parser.js';
|
|
|
48
56
|
export type TSessionSurfaceRole = 'drive' | 'observe';
|
|
49
57
|
|
|
50
58
|
export interface ISessionMessageHandlerOptions {
|
|
51
|
-
/** IProtocolSession to expose. */
|
|
52
|
-
session:
|
|
59
|
+
/** IProtocolSession to expose — `Partial<ISessionProjectRead>` is probed, not assumed (#3282 §4c). */
|
|
60
|
+
session: TProjectReadCapableSession;
|
|
53
61
|
/**
|
|
54
62
|
* ARCH-030: the CARRIER's connection-scoped outbound delivery boundary — not a raw `send`, and not a
|
|
55
63
|
* `send` plus an error callback for this handler to assemble into one. The carrier owns both the sink
|
|
@@ -76,6 +84,15 @@ export interface ISessionMessageHandlerOptions {
|
|
|
76
84
|
storedSessionUsageReporter?: NonNullable<IUsageQueryReporters['storedSessionUsageReporter']>;
|
|
77
85
|
/** Host-owned session directory (#3189): list, start and switch the host's sessions. */
|
|
78
86
|
sessionDirectory?: ISessionDirectory;
|
|
87
|
+
/** #3282 §4a: host-owned read/write for the GUI Settings screen. */
|
|
88
|
+
settingsReporter?: ISettingsReporter;
|
|
89
|
+
/**
|
|
90
|
+
* #3282 §4 part b-2: carrier-decided, like `role` — whether THIS carrier's connections are
|
|
91
|
+
* provably on this machine. The loopback WS carrier is `'local'`; the device-mesh/WebRTC carrier
|
|
92
|
+
* sets `'remote'`. Absent → `'local'`, forwarded to `executeCommand` and the settings reporter so a
|
|
93
|
+
* command that runs code from outside the session (installing a plugin) can refuse a remote device.
|
|
94
|
+
*/
|
|
95
|
+
commandSurfaceLocality?: TCommandSurfaceLocality;
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
/**
|
|
@@ -118,19 +135,23 @@ export function createSessionMessageHandler(options: ISessionMessageHandlerOptio
|
|
|
118
135
|
options.surface,
|
|
119
136
|
role,
|
|
120
137
|
options.sessionDirectory,
|
|
138
|
+
options.settingsReporter,
|
|
139
|
+
options.commandSurfaceLocality,
|
|
121
140
|
);
|
|
122
141
|
|
|
123
142
|
return { onMessage, cleanup };
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
function createMessageHandler(
|
|
127
|
-
session:
|
|
146
|
+
session: TProjectReadCapableSession,
|
|
128
147
|
deliver: TOutboundDeliver,
|
|
129
148
|
driverId?: TDriverId,
|
|
130
149
|
reporters: IUsageQueryReporters = EMPTY_USAGE_REPORTERS,
|
|
131
150
|
surface?: TUsageSurface,
|
|
132
151
|
role: TSessionSurfaceRole = 'drive',
|
|
133
152
|
sessionDirectory?: ISessionDirectory,
|
|
153
|
+
settingsReporter?: ISettingsReporter,
|
|
154
|
+
commandSurfaceLocality?: TCommandSurfaceLocality,
|
|
134
155
|
): (data: string) => void {
|
|
135
156
|
return (data: string): void => {
|
|
136
157
|
const msg = parseClientMessage(data, deliver);
|
|
@@ -139,7 +160,17 @@ function createMessageHandler(
|
|
|
139
160
|
deliver({ type: 'protocol_error', message: `Not permitted for an observer: ${msg.type}` });
|
|
140
161
|
return;
|
|
141
162
|
}
|
|
142
|
-
handleClientMessage(
|
|
163
|
+
handleClientMessage(
|
|
164
|
+
session,
|
|
165
|
+
deliver,
|
|
166
|
+
msg,
|
|
167
|
+
driverId,
|
|
168
|
+
reporters,
|
|
169
|
+
surface,
|
|
170
|
+
sessionDirectory,
|
|
171
|
+
settingsReporter,
|
|
172
|
+
commandSurfaceLocality,
|
|
173
|
+
);
|
|
143
174
|
};
|
|
144
175
|
}
|
|
145
176
|
|
|
@@ -154,13 +185,15 @@ const EMPTY_USAGE_REPORTERS: IUsageQueryReporters = {
|
|
|
154
185
|
* the {@link SessionResumeBridge} intercepts `resume`/`ack` itself and delegates everything else here.
|
|
155
186
|
*/
|
|
156
187
|
export function handleClientMessage(
|
|
157
|
-
session:
|
|
188
|
+
session: TProjectReadCapableSession,
|
|
158
189
|
deliver: TOutboundDeliver,
|
|
159
190
|
msg: TClientMessage,
|
|
160
191
|
driverId?: TDriverId,
|
|
161
192
|
reporters: IUsageQueryReporters = EMPTY_USAGE_REPORTERS,
|
|
162
193
|
surface?: TUsageSurface,
|
|
163
194
|
sessionDirectory?: ISessionDirectory,
|
|
195
|
+
settingsReporter?: ISettingsReporter,
|
|
196
|
+
commandSurfaceLocality?: TCommandSurfaceLocality,
|
|
164
197
|
): void {
|
|
165
198
|
if (handleUsageQueryMessage(session, deliver, msg, reporters)) {
|
|
166
199
|
return;
|
|
@@ -169,8 +202,20 @@ export function handleClientMessage(
|
|
|
169
202
|
handleSessionDirectoryMessage(deliver, msg, sessionDirectory);
|
|
170
203
|
return;
|
|
171
204
|
}
|
|
205
|
+
if (isSettingsMessage(msg)) {
|
|
206
|
+
handleSettingsMessage(session, deliver, msg, settingsReporter, commandSurfaceLocality);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (isSessionRenameMessage(msg)) {
|
|
210
|
+
handleSessionRenameMessage(deliver, msg, sessionDirectory);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (isSessionDeleteMessage(msg)) {
|
|
214
|
+
handleSessionDeleteMessage(deliver, msg, sessionDirectory);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
172
217
|
if (isSessionControlMessage(msg)) {
|
|
173
|
-
handleSessionControlMessage(session, deliver, msg, driverId, surface);
|
|
218
|
+
handleSessionControlMessage(session, deliver, msg, driverId, surface, commandSurfaceLocality);
|
|
174
219
|
return;
|
|
175
220
|
}
|
|
176
221
|
if (isLoopControlMessage(msg)) {
|
|
@@ -181,6 +226,10 @@ export function handleClientMessage(
|
|
|
181
226
|
handleSessionQueryMessage(session, deliver, msg);
|
|
182
227
|
return;
|
|
183
228
|
}
|
|
229
|
+
if (isProjectReadMessage(msg)) {
|
|
230
|
+
handleProjectReadMessage(session, deliver, msg);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
184
233
|
if (isBackgroundQueryMessage(msg)) {
|
|
185
234
|
handleBackgroundQueryMessage(session, deliver, msg);
|
|
186
235
|
return;
|
|
@@ -268,6 +317,7 @@ function handleSessionControlMessage(
|
|
|
268
317
|
msg: Extract<TClientMessage, { type: 'submit' | 'command' | 'abort' | 'cancel-queue' }>,
|
|
269
318
|
driverId?: TDriverId,
|
|
270
319
|
surface?: TUsageSurface,
|
|
320
|
+
commandSurfaceLocality?: TCommandSurfaceLocality,
|
|
271
321
|
): void {
|
|
272
322
|
if (msg.type === 'submit') {
|
|
273
323
|
// TRANS-008 (issue #2045). A TYPE check, not a falsy one: `{}`, `[]`, `42` and `true` are truthy
|
|
@@ -304,7 +354,7 @@ function handleSessionControlMessage(
|
|
|
304
354
|
}
|
|
305
355
|
// REMOTE-003: a transport-origin command is tagged `'remote'` (optional policy, allow-by-default;
|
|
306
356
|
// REMOTE-006). CMD-004: the SERVER-ASSIGNED driver id (E5) is the command origin — intents route back here.
|
|
307
|
-
session.executeCommand(msg.name, msg.args ?? '', 'remote', driverId).then(
|
|
357
|
+
session.executeCommand(msg.name, msg.args ?? '', 'remote', driverId, commandSurfaceLocality).then(
|
|
308
358
|
(result) => {
|
|
309
359
|
deliver({
|
|
310
360
|
type: 'command_result',
|
|
@@ -14,6 +14,8 @@ type TSessionQueryMessage = Extract<
|
|
|
14
14
|
| 'get-context'
|
|
15
15
|
| 'get-commands'
|
|
16
16
|
| 'get-status'
|
|
17
|
+
| 'list-models'
|
|
18
|
+
| 'get-agent-definitions'
|
|
17
19
|
| 'get-executing'
|
|
18
20
|
| 'get-pending'
|
|
19
21
|
| 'get-execution-workspace'
|
|
@@ -38,6 +40,8 @@ export function isSessionQueryMessage(msg: TClientMessage): msg is TSessionQuery
|
|
|
38
40
|
msg.type === 'get-context' ||
|
|
39
41
|
msg.type === 'get-commands' ||
|
|
40
42
|
msg.type === 'get-status' ||
|
|
43
|
+
msg.type === 'list-models' ||
|
|
44
|
+
msg.type === 'get-agent-definitions' ||
|
|
41
45
|
msg.type === 'get-executing' ||
|
|
42
46
|
msg.type === 'get-pending' ||
|
|
43
47
|
msg.type === 'get-execution-workspace' ||
|
|
@@ -51,7 +55,10 @@ export function handleSessionQueryMessage(
|
|
|
51
55
|
msg: TSessionQueryMessage,
|
|
52
56
|
): void {
|
|
53
57
|
if (msg.type === 'get-messages') {
|
|
54
|
-
|
|
58
|
+
// #3288 §2: `display` rides the same frame as `messages` — a reload/reconnect/resume replay
|
|
59
|
+
// needs both in one round trip, and there is no reason a client would ever want one without
|
|
60
|
+
// the other (the raw messages already have to be fetched to build the projection).
|
|
61
|
+
deliver({ type: 'messages', messages: session.getMessages(), display: session.getMessagesDisplay() });
|
|
55
62
|
} else if (msg.type === 'get-history') {
|
|
56
63
|
deliver(historyPage(session, msg.fromIndex ?? 0));
|
|
57
64
|
} else if (msg.type === 'get-prompts') {
|
|
@@ -62,6 +69,16 @@ export function handleSessionQueryMessage(
|
|
|
62
69
|
deliver({ type: 'commands', commands: session.listCommands(), skills: session.listSkills() });
|
|
63
70
|
} else if (msg.type === 'get-status') {
|
|
64
71
|
deliver({ type: 'session_status', status: session.getStatusSnapshot() });
|
|
72
|
+
} else if (msg.type === 'list-models') {
|
|
73
|
+
const snapshot = session.listModels();
|
|
74
|
+
deliver({ type: 'model_list', requestId: msg.requestId, ...snapshot });
|
|
75
|
+
} else if (msg.type === 'get-agent-definitions') {
|
|
76
|
+
deliver({
|
|
77
|
+
type: 'agent_definitions',
|
|
78
|
+
requestId: msg.requestId,
|
|
79
|
+
agents: session.listAgentDefinitions(),
|
|
80
|
+
current: session.getDefaultAgentType(),
|
|
81
|
+
});
|
|
65
82
|
} else if (msg.type === 'get-executing') {
|
|
66
83
|
deliver({ type: 'executing', executing: session.isExecuting() });
|
|
67
84
|
} else if (msg.type === 'get-execution-workspace') {
|
|
@@ -28,6 +28,7 @@ import type { TOutboundDeliver } from './outbound-delivery.js';
|
|
|
28
28
|
import type { IProtocolSession } from './protocol-session.js';
|
|
29
29
|
import type { IUsageQueryReporters } from './usage-messages.js';
|
|
30
30
|
import type { TSeqServerMessage, TServerMessage } from './wire-messages.js';
|
|
31
|
+
import type { TCommandSurfaceLocality } from '@robota-sdk/agent-interface-command';
|
|
31
32
|
import type { TUsageSurface } from '@robota-sdk/agent-interface-analytics';
|
|
32
33
|
import type { TDriverId } from '@robota-sdk/agent-interface-session';
|
|
33
34
|
|
|
@@ -65,6 +66,8 @@ export interface ISessionResumeBridgeOptions {
|
|
|
65
66
|
readonly personalUsageReporter?: IUsageQueryReporters['personalUsageReporter'];
|
|
66
67
|
readonly usageReporter?: IUsageQueryReporters['usageReporter'];
|
|
67
68
|
readonly storedSessionUsageReporter?: IUsageQueryReporters['storedSessionUsageReporter'];
|
|
69
|
+
/** #3282 §4 part b-2: this bridge is the reconnectable WebRTC path, so it is always `'remote'`. */
|
|
70
|
+
readonly commandSurfaceLocality?: TCommandSurfaceLocality;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export class SessionResumeBridge {
|
|
@@ -73,6 +76,7 @@ export class SessionResumeBridge {
|
|
|
73
76
|
private driverId?: TDriverId;
|
|
74
77
|
private readonly reporters: IUsageQueryReporters;
|
|
75
78
|
private readonly surface?: TUsageSurface;
|
|
79
|
+
private readonly commandSurfaceLocality?: TCommandSurfaceLocality;
|
|
76
80
|
private readonly unsubscribe: () => void;
|
|
77
81
|
private onDeliveryError?: IAttachOptions['onDeliveryError'];
|
|
78
82
|
/**
|
|
@@ -105,6 +109,7 @@ export class SessionResumeBridge {
|
|
|
105
109
|
this.buffer = new ResumeBuffer(options.buffer);
|
|
106
110
|
this.driverId = options.driverId;
|
|
107
111
|
this.surface = options.surface;
|
|
112
|
+
this.commandSurfaceLocality = options.commandSurfaceLocality;
|
|
108
113
|
this.reporters = {
|
|
109
114
|
personalUsageReporter: options.personalUsageReporter,
|
|
110
115
|
usageReporter: options.usageReporter,
|
|
@@ -178,6 +183,9 @@ export class SessionResumeBridge {
|
|
|
178
183
|
this.driverId,
|
|
179
184
|
this.reporters,
|
|
180
185
|
this.surface,
|
|
186
|
+
undefined,
|
|
187
|
+
undefined,
|
|
188
|
+
this.commandSurfaceLocality,
|
|
181
189
|
);
|
|
182
190
|
}
|
|
183
191
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { TOutboundDeliver } from './outbound-delivery.js';
|
|
2
|
+
import type { IProtocolSession } from './protocol-session.js';
|
|
3
|
+
import type { TClientMessage } from './wire-messages.js';
|
|
4
|
+
import type { TCommandSurfaceLocality } from '@robota-sdk/agent-interface-command';
|
|
5
|
+
import type { ISettingsSnapshot, TSettingsPatch } from '@robota-sdk/agent-interface-session';
|
|
6
|
+
|
|
7
|
+
type TSettingsMessage = Extract<TClientMessage, { type: 'get-settings' | 'update-settings' }>;
|
|
8
|
+
|
|
9
|
+
export function isSettingsMessage(msg: TClientMessage): msg is TSettingsMessage {
|
|
10
|
+
return msg.type === 'get-settings' || msg.type === 'update-settings';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** What applying a patch did: the fresh snapshot, or why nothing was written. */
|
|
14
|
+
export type TSettingsUpdateOutcome =
|
|
15
|
+
| { readonly ok: true; readonly settings: ISettingsSnapshot }
|
|
16
|
+
| {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly code: 'not_available' | 'invalid' | 'refused' | 'update_failed';
|
|
19
|
+
readonly message: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* #3282 §4a: host-owned read/write for the Settings screen, supplied by the composition root the
|
|
24
|
+
* same way `IUsageQueryReporters` supplies usage reads — this package only correlates the request
|
|
25
|
+
* and carries the result. `updateSettings` applies through the SAME function the matching slash
|
|
26
|
+
* command uses (issue #3282 §4: "one code path, so the behavior matches the TUI"), so whatever a
|
|
27
|
+
* command would refuse on this surface is refused here too.
|
|
28
|
+
*/
|
|
29
|
+
export interface ISettingsReporter {
|
|
30
|
+
getSettings(
|
|
31
|
+
session: IProtocolSession,
|
|
32
|
+
locality?: TCommandSurfaceLocality,
|
|
33
|
+
): ISettingsSnapshot | Promise<ISettingsSnapshot>;
|
|
34
|
+
/**
|
|
35
|
+
* Applies through the SAME function the matching slash command uses. `locality` (#3282 §4 part
|
|
36
|
+
* b-2) is forwarded to that command exactly as the transport received it, so a command that
|
|
37
|
+
* refuses a remote surface (installing a plugin) refuses identically here.
|
|
38
|
+
*/
|
|
39
|
+
updateSettings(
|
|
40
|
+
session: IProtocolSession,
|
|
41
|
+
patch: TSettingsPatch,
|
|
42
|
+
locality?: TCommandSurfaceLocality,
|
|
43
|
+
): TSettingsUpdateOutcome | Promise<TSettingsUpdateOutcome>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function handleSettingsMessage(
|
|
47
|
+
session: IProtocolSession,
|
|
48
|
+
deliver: TOutboundDeliver,
|
|
49
|
+
msg: TSettingsMessage,
|
|
50
|
+
reporter: ISettingsReporter | undefined,
|
|
51
|
+
locality?: TCommandSurfaceLocality,
|
|
52
|
+
): void {
|
|
53
|
+
if (!reporter) {
|
|
54
|
+
deliverSettingsError(
|
|
55
|
+
deliver,
|
|
56
|
+
msg.requestId,
|
|
57
|
+
'not_available',
|
|
58
|
+
'Settings are not available on this host.',
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
if (msg.type === 'get-settings') {
|
|
64
|
+
void Promise.resolve(reporter.getSettings(session, locality)).then(
|
|
65
|
+
(settings) => deliver({ type: 'settings', requestId: msg.requestId, settings }),
|
|
66
|
+
(error) =>
|
|
67
|
+
deliverSettingsError(deliver, msg.requestId, 'update_failed', failureMessage(error)),
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
void Promise.resolve(reporter.updateSettings(session, msg.patch, locality)).then(
|
|
72
|
+
(outcome) => {
|
|
73
|
+
if (outcome.ok) {
|
|
74
|
+
deliver({ type: 'settings', requestId: msg.requestId, settings: outcome.settings });
|
|
75
|
+
} else {
|
|
76
|
+
deliverSettingsError(deliver, msg.requestId, outcome.code, outcome.message);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
(error) =>
|
|
80
|
+
deliverSettingsError(deliver, msg.requestId, 'update_failed', failureMessage(error)),
|
|
81
|
+
);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
deliverSettingsError(
|
|
84
|
+
deliver,
|
|
85
|
+
msg.requestId,
|
|
86
|
+
'update_failed',
|
|
87
|
+
failureMessage(error instanceof Error ? error : String(error)),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function deliverSettingsError(
|
|
93
|
+
deliver: TOutboundDeliver,
|
|
94
|
+
requestId: string,
|
|
95
|
+
code: 'not_available' | 'invalid' | 'refused' | 'update_failed',
|
|
96
|
+
message: string,
|
|
97
|
+
): void {
|
|
98
|
+
deliver({ type: 'settings_error', requestId, code, message });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function failureMessage(error: Error | string): string {
|
|
102
|
+
return error instanceof Error ? error.message : String(error);
|
|
103
|
+
}
|
package/src/wire-messages.ts
CHANGED
|
@@ -28,17 +28,24 @@ import type {
|
|
|
28
28
|
IBranchEvent,
|
|
29
29
|
IExecutionResult,
|
|
30
30
|
IContextFileRefreshedEvent,
|
|
31
|
+
IModelListGroup,
|
|
31
32
|
IPermissionRequestEvent,
|
|
32
33
|
IPromptResolvedEvent,
|
|
33
34
|
IPlanApprovalEvent,
|
|
34
35
|
ISessionRenamedEvent,
|
|
36
|
+
ISettingsSnapshot,
|
|
35
37
|
IToolState,
|
|
36
38
|
ISessionListing,
|
|
37
39
|
ISessionStatusSnapshot,
|
|
38
40
|
ISessionSwitchedEvent,
|
|
39
41
|
TSessionChangeRefusalCode,
|
|
42
|
+
TSessionDeleteRefusalCode,
|
|
43
|
+
TSettingsPatch,
|
|
40
44
|
IUiIntentEvent,
|
|
41
45
|
TPermissionResultValue,
|
|
46
|
+
TProjectDiffRead,
|
|
47
|
+
TProjectMemoryRead,
|
|
48
|
+
TProjectStatusRead,
|
|
42
49
|
TWaitingLoopStopOutcome,
|
|
43
50
|
} from '@robota-sdk/agent-interface-session';
|
|
44
51
|
import type {
|
|
@@ -52,6 +59,16 @@ export type TBackgroundControlAction = 'cancel' | 'close' | 'send';
|
|
|
52
59
|
|
|
53
60
|
type THistoryEntry = ReturnType<ISessionConversationRead['getFullHistory']>[number];
|
|
54
61
|
|
|
62
|
+
/**
|
|
63
|
+
* #3282 §4: one agent the switcher can offer. `definedIn` is a plain-words location — a discovered
|
|
64
|
+
* file's path, or "Built-in" — never a raw internal id.
|
|
65
|
+
*/
|
|
66
|
+
export interface IWireAgentDefinitionSummary {
|
|
67
|
+
name: string;
|
|
68
|
+
description: string;
|
|
69
|
+
definedIn: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
/**
|
|
56
73
|
* #3189: one entry of the session's full history as it crosses the wire. The same record the session
|
|
57
74
|
* keeps, except that `timestamp` is an ISO 8601 string: a `Date` does not survive JSON, and a
|
|
@@ -89,11 +106,25 @@ export type TClientMessage =
|
|
|
89
106
|
// (a `/` menu), and the session's status (model, permission mode, effort, context).
|
|
90
107
|
| { type: 'get-commands' }
|
|
91
108
|
| { type: 'get-status' }
|
|
109
|
+
// #3282 §2: the models the GUI's model menu offers, grouped by configured provider profile — so
|
|
110
|
+
// the GUI never scrapes `/model`'s picker text. Choosing one sends `command` with `name: 'model'`,
|
|
111
|
+
// the SAME path `/model <id>` runs; this message only reads the choices.
|
|
112
|
+
| { type: 'list-models'; requestId: string }
|
|
113
|
+
// #3282 §4: the agent switcher's roster and its currently-selected default — so the GUI never
|
|
114
|
+
// scrapes `/agent`'s text. Choosing one sends `command` with `name: 'agent'`, the SAME path
|
|
115
|
+
// `/agent <name>` runs; this message only reads the roster and the current selection.
|
|
116
|
+
| { type: 'get-agent-definitions'; requestId: string }
|
|
92
117
|
// #3189: the host's sessions — list them, start a new one, make another current. A refused change
|
|
93
118
|
// answers `session_change_failed` with the same `requestId`.
|
|
94
119
|
| { type: 'list-sessions'; requestId: string }
|
|
95
120
|
| { type: 'new-session'; requestId?: string }
|
|
96
121
|
| { type: 'switch-session'; sessionId: string; requestId?: string }
|
|
122
|
+
// #3289 §1: rename a session from the list — current or not. Answered by `session_renamed_in_list`
|
|
123
|
+
// or `session_rename_failed`, both carrying the same `requestId`.
|
|
124
|
+
| { type: 'rename-session'; sessionId: string; name: string; requestId: string }
|
|
125
|
+
// #3289 §1: remove a stored session's record for good. Answered by `session_deleted` or
|
|
126
|
+
// `session_delete_failed`, both carrying the same `requestId`.
|
|
127
|
+
| { type: 'delete-session'; sessionId: string; requestId: string }
|
|
97
128
|
// SELFHOST-004: request the assembled trace/cost read-model (spans + cost-by-source) for the run.
|
|
98
129
|
| { type: 'get-usage-report' }
|
|
99
130
|
| {
|
|
@@ -116,6 +147,16 @@ export type TClientMessage =
|
|
|
116
147
|
}
|
|
117
148
|
// Stop the self-paced loop that is waiting for its next wake; answered by `waiting_loop_stop`.
|
|
118
149
|
| { type: 'stop-waiting-loop'; requestId: string }
|
|
150
|
+
// #3282 §4c: the Project panel's "Changes" section — this workspace's git status. Answered by
|
|
151
|
+
// `project_status`, echoing `requestId`.
|
|
152
|
+
| { type: 'project-status'; requestId: string }
|
|
153
|
+
// The Project panel's "File diff" section for one file `project_status` reported — `path` exactly
|
|
154
|
+
// as reported (workspace-relative; the server re-checks it stays inside the workspace). Answered by
|
|
155
|
+
// `project_diff`.
|
|
156
|
+
| { type: 'project-diff'; requestId: string; path: string }
|
|
157
|
+
// The Project panel's "Memory" section — project memory as readable text. Answered by
|
|
158
|
+
// `project_memory`.
|
|
159
|
+
| { type: 'project-memory'; requestId: string }
|
|
119
160
|
| { type: 'get-background-tasks'; filter?: IBackgroundTaskListFilter }
|
|
120
161
|
| { type: 'get-background-task'; taskId: string }
|
|
121
162
|
| { type: 'get-background-job-groups' }
|
|
@@ -131,7 +172,12 @@ export type TClientMessage =
|
|
|
131
172
|
// REMOTE-013 E4 session-resume: `resume` asks the host to replay the tail after `lastSeq` (the last seq the
|
|
132
173
|
// client applied); `ack` lets the host free its un-acked buffer up to `seq`. Only meaningful post-E3-accept.
|
|
133
174
|
| { type: 'resume'; lastSeq: number }
|
|
134
|
-
| { type: 'ack'; seq: number }
|
|
175
|
+
| { type: 'ack'; seq: number }
|
|
176
|
+
// #3282 §4a: the GUI Settings screen. A snapshot fetch and a discriminated single-field patch,
|
|
177
|
+
// never command text — the server applies a patch through the same function its slash command
|
|
178
|
+
// uses, so the two paths cannot drift. Answered by `settings` or `settings_error`.
|
|
179
|
+
| { type: 'get-settings'; requestId: string }
|
|
180
|
+
| { type: 'update-settings'; requestId: string; patch: TSettingsPatch };
|
|
135
181
|
|
|
136
182
|
/** Outbound message from server to client. */
|
|
137
183
|
export type TServerMessage =
|
|
@@ -145,7 +191,28 @@ export type TServerMessage =
|
|
|
145
191
|
| { type: 'thinking'; isThinking: boolean; driverId?: TDriverId }
|
|
146
192
|
| { type: 'complete'; result: TWireExecutionResult; driverId?: TDriverId }
|
|
147
193
|
| { type: 'interrupted'; result: TWireExecutionResult; driverId?: TDriverId }
|
|
148
|
-
| {
|
|
194
|
+
| {
|
|
195
|
+
type: 'error';
|
|
196
|
+
/** The raw failure text — kept for a "Details" disclosure; never the whole story on its own. */
|
|
197
|
+
message: string;
|
|
198
|
+
/**
|
|
199
|
+
* What kind of failure this was, when it's known — so a renderer can say what happened and
|
|
200
|
+
* what to do next in plain words instead of showing `message` verbatim (#3289 §3). Additive:
|
|
201
|
+
* absent for a session error this classification does not recognize (unchanged behavior).
|
|
202
|
+
*/
|
|
203
|
+
code?: 'auth' | 'rate_limit' | 'model_unavailable' | 'network' | 'provider';
|
|
204
|
+
/** The provider/profile name the failure came from, when `code` names one. */
|
|
205
|
+
provider?: string;
|
|
206
|
+
/** For `code: 'rate_limit'`, how long until a retry may succeed, when the provider said so. */
|
|
207
|
+
retryAfterSeconds?: number;
|
|
208
|
+
/**
|
|
209
|
+
* For `code: 'model_unavailable'`, the model the failed request tried — captured at the moment
|
|
210
|
+
* of failure so a renderer never has to fall back to whatever model is live by the time it
|
|
211
|
+
* draws this notice (which may already be a different one the person switched to).
|
|
212
|
+
*/
|
|
213
|
+
model?: string;
|
|
214
|
+
driverId?: TDriverId;
|
|
215
|
+
}
|
|
149
216
|
| {
|
|
150
217
|
type: 'command_result';
|
|
151
218
|
name: string;
|
|
@@ -155,7 +222,24 @@ export type TServerMessage =
|
|
|
155
222
|
/** The `requestId` of the `command` this answers, when it carried one. */
|
|
156
223
|
requestId?: string;
|
|
157
224
|
}
|
|
158
|
-
| {
|
|
225
|
+
| {
|
|
226
|
+
type: 'messages';
|
|
227
|
+
messages: ReturnType<ISessionConversationRead['getMessages']>;
|
|
228
|
+
/**
|
|
229
|
+
* #3288 §2: the SAME history, projected into display segments (text runs and finished tool
|
|
230
|
+
* calls, diffs included) — so a reload/reconnect/resume replay shows tool rows instead of bare
|
|
231
|
+
* text. Optional so an older host that has not grown `getMessagesDisplay()` yet still sends a
|
|
232
|
+
* valid frame; a client without this field falls back to `messages`-only (text bubbles), the
|
|
233
|
+
* same as before this existed.
|
|
234
|
+
*/
|
|
235
|
+
display?: ReturnType<ISessionConversationRead['getMessagesDisplay']>;
|
|
236
|
+
/**
|
|
237
|
+
* #3289 §3: this connection's own server-assigned driver id, carried on the first frame every
|
|
238
|
+
* connection already gets — so a client can tell its OWN turns from a co-driver's instead of
|
|
239
|
+
* treating everything but the literal `'owner'` id as someone else (REMOTE-014 E5 display-only).
|
|
240
|
+
*/
|
|
241
|
+
driverId?: TDriverId;
|
|
242
|
+
}
|
|
159
243
|
// #3189: one page of the full history. `entries` start at `startIndex` of the `total` the session
|
|
160
244
|
// holds now, and stop before a page grows past a bounded size (a single larger entry is sent alone).
|
|
161
245
|
| { type: 'history'; startIndex: number; total: number; entries: IWireHistoryEntry[] }
|
|
@@ -169,6 +253,23 @@ export type TServerMessage =
|
|
|
169
253
|
| { type: 'commands'; commands: ICommandListEntry[]; skills: ICommandSkillListEntry[] }
|
|
170
254
|
// Sent in reply to `get-status`, and pushed whenever the session's status changes.
|
|
171
255
|
| { type: 'session_status'; status: ISessionStatusSnapshot }
|
|
256
|
+
// #3282 §2: reply to `list-models`, echoing its `requestId`. `groups` is current-profile first;
|
|
257
|
+
// `currentProfile` is absent only when no provider profile is configured at all.
|
|
258
|
+
| {
|
|
259
|
+
type: 'model_list';
|
|
260
|
+
requestId: string;
|
|
261
|
+
groups: readonly IModelListGroup[];
|
|
262
|
+
currentProfile?: string;
|
|
263
|
+
currentModel: string;
|
|
264
|
+
}
|
|
265
|
+
// #3282 §4: reply to `get-agent-definitions`, echoing its `requestId`. `current` is always one of
|
|
266
|
+
// `agents`' names (the fallback default when nothing was ever selected).
|
|
267
|
+
| {
|
|
268
|
+
type: 'agent_definitions';
|
|
269
|
+
requestId: string;
|
|
270
|
+
agents: readonly IWireAgentDefinitionSummary[];
|
|
271
|
+
current: string;
|
|
272
|
+
}
|
|
172
273
|
| { type: 'sessions'; requestId: string; listing: ISessionListing }
|
|
173
274
|
| {
|
|
174
275
|
type: 'sessions_error';
|
|
@@ -186,6 +287,18 @@ export type TServerMessage =
|
|
|
186
287
|
message: string;
|
|
187
288
|
requestId?: string;
|
|
188
289
|
}
|
|
290
|
+
// #3289 §1: a `rename-session` succeeded (current session or not); the client refreshes its listing.
|
|
291
|
+
| { type: 'session_renamed_in_list'; requestId: string; sessionId: string; name: string }
|
|
292
|
+
| { type: 'session_rename_failed'; requestId: string; message: string }
|
|
293
|
+
// #3289 §1: a `delete-session` succeeded; the client refreshes its listing and, if it was showing
|
|
294
|
+
// the deleted session, re-reads what it now shows (the host has already switched it away).
|
|
295
|
+
| { type: 'session_deleted'; requestId: string; sessionId: string }
|
|
296
|
+
| {
|
|
297
|
+
type: 'session_delete_failed';
|
|
298
|
+
requestId: string;
|
|
299
|
+
code: TSessionDeleteRefusalCode;
|
|
300
|
+
message: string;
|
|
301
|
+
}
|
|
189
302
|
// SELFHOST-004 (P5, TC-08): carry the assembled trace/cost read-model (per-op span timeline +
|
|
190
303
|
// cost-by-source) across the sidecar boundary — no existing variant carries per-op `durationMs` or
|
|
191
304
|
// per-source `costUsd`. The GUI renders it renderer-side.
|
|
@@ -218,6 +331,13 @@ export type TServerMessage =
|
|
|
218
331
|
| { type: 'execution_detail'; requestId: string; page: IExecutionDetailPage }
|
|
219
332
|
| { type: 'execution_detail_error'; requestId: string; message: string }
|
|
220
333
|
| { type: 'waiting_loop_stop'; requestId: string; outcome: TWaitingLoopStopOutcome }
|
|
334
|
+
// #3282 §4c: the Project panel's reads. Each result crosses the wire UNCHANGED as the session
|
|
335
|
+
// method's own return value (same convention as `waiting_loop_stop`'s `outcome` above) — the
|
|
336
|
+
// `kind` discriminant carries the non-repository / outside-workspace / unavailable / failed cases,
|
|
337
|
+
// so there is no separate `_error` sibling to keep in sync with these.
|
|
338
|
+
| { type: 'project_status'; requestId: string; result: TProjectStatusRead }
|
|
339
|
+
| { type: 'project_diff'; requestId: string; result: TProjectDiffRead }
|
|
340
|
+
| { type: 'project_memory'; requestId: string; result: TProjectMemoryRead }
|
|
221
341
|
| { type: 'background_task_event'; event: TBackgroundTaskEvent }
|
|
222
342
|
| { type: 'background_job_group_event'; event: TBackgroundJobGroupEvent }
|
|
223
343
|
| { type: 'plan_event'; event: IPlanApprovalEvent }
|
|
@@ -252,7 +372,16 @@ export type TServerMessage =
|
|
|
252
372
|
| { type: 'protocol_error'; message: string; requestId?: string }
|
|
253
373
|
// REMOTE-013 E4: sent instead of a replay when the client's `lastSeq` predates the host's retained buffer
|
|
254
374
|
// (overrun) — the client must do a full `get-messages` refresh rather than accept a silent gap.
|
|
255
|
-
| { type: 'resume_gap' }
|
|
375
|
+
| { type: 'resume_gap' }
|
|
376
|
+
// #3282 §4a: the Settings screen's snapshot, in reply to `get-settings` or `update-settings`. The
|
|
377
|
+
// same type answers both, the way `session_status` answers `get-status` and a status change alike.
|
|
378
|
+
| { type: 'settings'; requestId: string; settings: ISettingsSnapshot }
|
|
379
|
+
| {
|
|
380
|
+
type: 'settings_error';
|
|
381
|
+
requestId: string;
|
|
382
|
+
code: 'not_available' | 'invalid' | 'refused' | 'update_failed';
|
|
383
|
+
message: string;
|
|
384
|
+
};
|
|
256
385
|
|
|
257
386
|
/**
|
|
258
387
|
* REMOTE-013 E4: a server message stamped with its monotonic session sequence number (added by the
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{SESSION_CHANGE_REFUSAL_CODES as e}from"@robota-sdk/agent-interface-session";const t=4*1024*1024;function n(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}const r=e=>typeof e==`string`,i=e=>typeof e==`string`&&e.length>0,a=e=>typeof e==`boolean`,o=e=>typeof e==`number`&&Number.isFinite(e),s=e=>Number.isSafeInteger(e)&&e>=0,c=e=>t=>t===void 0||e(t),l=e=>Array.isArray(e)&&e.every(n),u=e=>e===null||n(e),d=e=>t=>typeof t==`string`&&e.includes(t),f={user:!0,"agent-wakeup":!0,peer:!0,external:!0},p=e=>n(e)&&r(e.id)&&r(e.timestamp)&&r(e.category)&&r(e.type),m=e=>Array.isArray(e)&&e.every(p),h=[`agent`,`process`,`scheduled`],g=[`foreground`,`background`],_=[`queued`,`running`,`waiting_permission`,`sleeping`,`paused`,`completed`,`failed`,`cancelled`],v=[`cancel`,`close`,`send`],y=e=>n(e)&&c(d(h))(e.kind)&&c(d(_))(e.status)&&c(d(g))(e.mode)&&c(a)(e.includeClosed),b=e=>n(e)&&c(r)(e.prompt)&&c(r)(e.stdin),x=e=>n(e)&&o(e.offset),S=e=>n(e)&&s(e.offset),C=e=>a(e)||e===`allow-session`||e===`allow-project`,w=e=>n(e)&&(e.type===`answer`&&Array.isArray(e.values)&&e.values.every(r)&&c(r)(e.text)||e.type===`cancelled`&&e.values===void 0&&e.text===void 0),T={none:{},several:{message:r},stopped:{loopId:i,message:c(r)},failed:{loopId:c(i),message:r}},E=e=>{if(!n(e)||!r(e.kind))return!1;let t=e.kind;if(!Object.prototype.hasOwnProperty.call(T,t))return!1;let i=T[t];return Object.entries(i).every(([t,n])=>n(e[t]))},D={submit:{prompt:i},command:{name:i,args:c(r),requestId:c(i)},abort:{},"cancel-queue":{},"get-messages":{},"get-history":{fromIndex:c(s)},"get-prompts":{},"get-context":{},"get-commands":{},"get-status":{},"list-sessions":{requestId:i},"new-session":{requestId:c(i)},"switch-session":{sessionId:i,requestId:c(i)},"get-usage-report":{},"get-personal-usage-report":{requestId:i,period:d([`7d`,`30d`]),timezone:i},"get-stored-session-usage-report":{requestId:i,sessionId:i},"get-executing":{},"get-pending":{},"get-execution-workspace":{},"read-execution-detail":{requestId:i,entryId:i,cursor:c(S)},"stop-waiting-loop":{requestId:i},"get-background-tasks":{filter:c(y)},"get-background-task":{taskId:i},"get-background-job-groups":{},"get-background-job-group":{groupId:i},"wait-background-job-group":{groupId:i},"cancel-background-task":{taskId:i,reason:c(r)},"close-background-task":{taskId:i},"send-background-task":{taskId:i,input:b},"read-background-task-log":{taskId:i,cursor:c(x)},"permission-response":{id:i,result:C},"ask-response":{id:i,response:w},resume:{lastSeq:o},ack:{seq:o}},O={driverId:c(r)},k={text_delta:{delta:r,...O},user_message:{content:r,...O},tool_start:{state:n,...O},tool_end:{state:n,...O},thinking:{isThinking:a,...O},complete:{result:n,...O},interrupted:{result:n,...O},error:{message:r,...O},command_result:{name:r,message:r,success:a,requestId:c(r)},messages:{messages:l},history:{startIndex:s,total:s,entries:m},context:{state:n},history_changed:{},turn_source:{source:d(Object.keys(f))},commands:{commands:l,skills:l},session_status:{status:n},sessions:{requestId:i,listing:n},sessions_error:{requestId:i,code:d([`not_available`,`list_failed`]),message:r},session_switched:{event:n},session_change_failed:{code:d(e),message:r,requestId:c(r)},usage_report:{report:n},personal_usage_report:{requestId:i,report:n},personal_usage_report_error:{requestId:i,code:d([`not_available`,`report_failed`]),message:r},stored_session_usage_report:{requestId:i,sessionId:i,report:n},stored_session_usage_report_error:{requestId:i,sessionId:i,code:d([`not_available`,`report_failed`]),message:r},executing:{executing:a},pending:{pending:e=>e===null||r(e),pendingCount:c(s)},execution_workspace_event:{snapshot:n},execution_detail:{requestId:i,page:n},execution_detail_error:{requestId:i,message:r},waiting_loop_stop:{requestId:i,outcome:E},background_task_event:{event:n},background_job_group_event:{event:n},plan_event:{event:n},context_file_refreshed:{event:n},branch_event:{event:n},background_tasks:{tasks:l},background_task:{taskId:r,task:u},background_job_groups:{groups:l},background_job_group:{groupId:r,group:u},background_task_log:{taskId:r,page:n},permission_request:{event:n},ask_request:{event:n},prompt_resolved:{event:n},ui_intent:{event:n},session_renamed:{event:n},history_cleared:{},background_task_control_result:{action:d(v),taskId:r,success:a,message:c(r)},protocol_error:{message:r,requestId:c(r)},resume_gap:{}};function A(e,t,i){if(!n(e))return{ok:!1,reason:`message must be a JSON object`};let a=e.type;if(!r(a))return{ok:!1,reason:`message type must be a string`};let o=Object.prototype.hasOwnProperty.call(t,a)?t[a]:void 0;if(o===void 0)return{ok:!1,reason:`Unknown message type: ${a}`};for(let[t,n]of Object.entries(o))if(!n(e[t]))return{ok:!1,reason:i(t)};return{ok:!0,message:e}}function j(e){return A(e,D,e=>e===`prompt`?`prompt must be a non-empty string`:`${e} has an invalid shape`)}function M(e){return A(e,k,e=>`${e} has an invalid shape`)}function N(e,t){if(e.length>4194304)return{ok:!1,reason:`Frame too large`};let n;try{n=JSON.parse(e)}catch{return{ok:!1,reason:`Invalid JSON`}}return t(n)}const P=new Set([`get-messages`,`get-history`,`get-context`,`get-status`,`get-commands`,`get-executing`,`get-pending`,`get-execution-workspace`,`read-execution-detail`,`get-usage-report`,`list-sessions`,`get-background-tasks`,`get-background-task`,`get-background-job-groups`,`get-background-job-group`,`wait-background-job-group`,`read-background-task-log`]);function F(e){return P.has(e)}export{M as a,N as i,t as n,j as r,F as t};
|
|
2
|
-
//# sourceMappingURL=observer-messages-Bx-xReUQ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"observer-messages-Bx-xReUQ.js","names":[],"sources":["../../src/message-decoders.ts","../../src/observer-messages.ts"],"sourcesContent":["/**\n * Owner-side runtime decoders for the wire message unions (issue #2045 / TRANS-005 adjacent).\n *\n * `wire-messages.ts` owns the TypeScript unions; this file owns turning `JSON.parse` output into one\n * of them. Before it, the server did `JSON.parse(data) as TClientMessage` and each client did the\n * same for `TServerMessage`, so valid JSON with an invalid shape crossed the boundary wearing a\n * protocol type. Carriers now implement `raw string → owner decoder → typed message` and answer a\n * refusal with the protocol's own `protocol_error` (server side) or client error (client side).\n *\n * `decodeClientMessage` is TOTAL: every variant and every nested field it carries is validated.\n * `decodeServerMessage` validates every variant's discriminator and declared top-level fields; a\n * payload OWNED BY ANOTHER PACKAGE (`IToolState`, `IExecutionResult`, background events, …) is\n * checked as a non-array record (or array of records) here — its own total decoding belongs to its\n * owner and is tracked there, not duplicated in a transport package.\n */\n\nimport { SESSION_CHANGE_REFUSAL_CODES } from '@robota-sdk/agent-interface-session';\n\nimport type { TBackgroundControlAction, TClientMessage, TServerMessage } from './wire-messages.js';\n\nexport type TMessageDecodeResult<TMessage> =\n | { readonly ok: true; readonly message: TMessage }\n | { readonly ok: false; readonly reason: string };\n\n/** Serialized frames larger than this are refused before any parse (a bounded inbound budget). */\nexport const MAX_INBOUND_FRAME_BYTES = 4 * 1024 * 1024;\n\ntype TRecord = Record<string, unknown>;\n\nfunction isRecord(value: unknown): value is TRecord {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\nconst isString = (v: unknown): v is string => typeof v === 'string';\nconst isNonEmptyString = (v: unknown): v is string => typeof v === 'string' && v.length > 0;\nconst isBoolean = (v: unknown): v is boolean => typeof v === 'boolean';\nconst isFiniteNumber = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\nconst isCount = (v: unknown): v is number => Number.isSafeInteger(v) && (v as number) >= 0;\nconst isOptional =\n (check: (v: unknown) => boolean) =>\n (v: unknown): boolean =>\n v === undefined || check(v);\nconst isRecordArray = (v: unknown): boolean => Array.isArray(v) && v.every(isRecord);\nconst isNullableRecord = (v: unknown): boolean => v === null || isRecord(v);\nconst oneOf =\n (members: readonly string[]) =>\n (v: unknown): boolean =>\n typeof v === 'string' && members.includes(v);\n/** A record, not an array, so a turn source added to the union without one here fails to compile. */\nconst TURN_SOURCES: Readonly<\n Record<Extract<TServerMessage, { type: 'turn_source' }>['source'], true>\n> = { user: true, 'agent-wakeup': true, peer: true, external: true };\n/** A history entry's envelope is this package's (`IWireHistoryEntry`); its `data` is the recorder's. */\nconst isWireHistoryEntry = (v: unknown): boolean =>\n isRecord(v) &&\n isString(v['id']) &&\n isString(v['timestamp']) &&\n isString(v['category']) &&\n isString(v['type']);\nconst isWireHistoryEntries = (v: unknown): boolean =>\n Array.isArray(v) && v.every(isWireHistoryEntry);\n\ntype TFieldCheck = (value: unknown) => boolean;\ntype TVariantShape = Readonly<Record<string, TFieldCheck>>;\n\nconst BACKGROUND_TASK_KINDS = ['agent', 'process', 'scheduled'] as const;\nconst BACKGROUND_TASK_MODES = ['foreground', 'background'] as const;\nconst BACKGROUND_TASK_STATUSES = [\n 'queued',\n 'running',\n 'waiting_permission',\n 'sleeping',\n 'paused',\n 'completed',\n 'failed',\n 'cancelled',\n] as const;\nconst BACKGROUND_CONTROL_ACTIONS: readonly TBackgroundControlAction[] = ['cancel', 'close', 'send'];\n\nconst isBackgroundTaskListFilter: TFieldCheck = (v) =>\n isRecord(v) &&\n isOptional(oneOf(BACKGROUND_TASK_KINDS))(v['kind']) &&\n isOptional(oneOf(BACKGROUND_TASK_STATUSES))(v['status']) &&\n isOptional(oneOf(BACKGROUND_TASK_MODES))(v['mode']) &&\n isOptional(isBoolean)(v['includeClosed']);\nconst isBackgroundTaskInput: TFieldCheck = (v) =>\n isRecord(v) && isOptional(isString)(v['prompt']) && isOptional(isString)(v['stdin']);\nconst isBackgroundTaskLogCursor: TFieldCheck = (v) => isRecord(v) && isFiniteNumber(v['offset']);\nconst isExecutionDetailCursor: TFieldCheck = (v) => isRecord(v) && isCount(v['offset']);\nconst isPermissionResultValue: TFieldCheck = (v) =>\n isBoolean(v) || v === 'allow-session' || v === 'allow-project';\nconst isActionResponse: TFieldCheck = (v) =>\n isRecord(v) &&\n ((v['type'] === 'answer' &&\n Array.isArray(v['values']) &&\n v['values'].every(isString) &&\n isOptional(isString)(v['text'])) ||\n (v['type'] === 'cancelled' && v['values'] === undefined && v['text'] === undefined));\n\ntype TWaitingLoopStopOutcome = Extract<TServerMessage, { type: 'waiting_loop_stop' }>['outcome'];\n/** Keyed by `kind`, so an outcome added to the union without a shape here fails to compile. */\nconst WAITING_LOOP_STOP_OUTCOME_SHAPES: Readonly<\n Record<TWaitingLoopStopOutcome['kind'], TVariantShape>\n> = {\n none: {},\n several: { message: isString },\n stopped: { loopId: isNonEmptyString, message: isOptional(isString) },\n failed: { loopId: isOptional(isNonEmptyString), message: isString },\n};\nconst isWaitingLoopStopOutcome: TFieldCheck = (v) => {\n if (!isRecord(v) || !isString(v['kind'])) return false;\n const kind = v['kind'];\n if (!Object.prototype.hasOwnProperty.call(WAITING_LOOP_STOP_OUTCOME_SHAPES, kind)) return false;\n const shape = WAITING_LOOP_STOP_OUTCOME_SHAPES[kind as TWaitingLoopStopOutcome['kind']];\n return Object.entries(shape).every(([field, check]) => check(v[field]));\n};\n\n/** One entry per `TClientMessage` variant — a variant added to the union without one fails the test. */\nexport const CLIENT_MESSAGE_SHAPES: Readonly<Record<TClientMessage['type'], TVariantShape>> = {\n submit: { prompt: isNonEmptyString },\n command: {\n name: isNonEmptyString,\n args: isOptional(isString),\n requestId: isOptional(isNonEmptyString),\n },\n abort: {},\n 'cancel-queue': {},\n 'get-messages': {},\n 'get-history': { fromIndex: isOptional(isCount) },\n 'get-prompts': {},\n 'get-context': {},\n 'get-commands': {},\n 'get-status': {},\n 'list-sessions': { requestId: isNonEmptyString },\n 'new-session': { requestId: isOptional(isNonEmptyString) },\n 'switch-session': { sessionId: isNonEmptyString, requestId: isOptional(isNonEmptyString) },\n 'get-usage-report': {},\n 'get-personal-usage-report': {\n requestId: isNonEmptyString,\n period: oneOf(['7d', '30d']),\n timezone: isNonEmptyString,\n },\n 'get-stored-session-usage-report': {\n requestId: isNonEmptyString,\n sessionId: isNonEmptyString,\n },\n 'get-executing': {},\n 'get-pending': {},\n 'get-execution-workspace': {},\n 'read-execution-detail': {\n requestId: isNonEmptyString,\n entryId: isNonEmptyString,\n cursor: isOptional(isExecutionDetailCursor),\n },\n 'stop-waiting-loop': { requestId: isNonEmptyString },\n 'get-background-tasks': { filter: isOptional(isBackgroundTaskListFilter) },\n 'get-background-task': { taskId: isNonEmptyString },\n 'get-background-job-groups': {},\n 'get-background-job-group': { groupId: isNonEmptyString },\n 'wait-background-job-group': { groupId: isNonEmptyString },\n 'cancel-background-task': { taskId: isNonEmptyString, reason: isOptional(isString) },\n 'close-background-task': { taskId: isNonEmptyString },\n 'send-background-task': { taskId: isNonEmptyString, input: isBackgroundTaskInput },\n 'read-background-task-log': {\n taskId: isNonEmptyString,\n cursor: isOptional(isBackgroundTaskLogCursor),\n },\n 'permission-response': { id: isNonEmptyString, result: isPermissionResultValue },\n 'ask-response': { id: isNonEmptyString, response: isActionResponse },\n resume: { lastSeq: isFiniteNumber },\n ack: { seq: isFiniteNumber },\n};\n\nconst authored: TVariantShape = { driverId: isOptional(isString) };\n\n/** One entry per `TServerMessage` variant. Foreign-owned payloads are checked as records (see header). */\nexport const SERVER_MESSAGE_SHAPES: Readonly<Record<TServerMessage['type'], TVariantShape>> = {\n text_delta: { delta: isString, ...authored },\n user_message: { content: isString, ...authored },\n tool_start: { state: isRecord, ...authored },\n tool_end: { state: isRecord, ...authored },\n thinking: { isThinking: isBoolean, ...authored },\n complete: { result: isRecord, ...authored },\n interrupted: { result: isRecord, ...authored },\n error: { message: isString, ...authored },\n command_result: {\n name: isString,\n message: isString,\n success: isBoolean,\n requestId: isOptional(isString),\n },\n messages: { messages: isRecordArray },\n history: { startIndex: isCount, total: isCount, entries: isWireHistoryEntries },\n context: { state: isRecord },\n history_changed: {},\n turn_source: { source: oneOf(Object.keys(TURN_SOURCES)) },\n commands: { commands: isRecordArray, skills: isRecordArray },\n session_status: { status: isRecord },\n sessions: { requestId: isNonEmptyString, listing: isRecord },\n sessions_error: {\n requestId: isNonEmptyString,\n code: oneOf(['not_available', 'list_failed']),\n message: isString,\n },\n session_switched: { event: isRecord },\n session_change_failed: {\n code: oneOf(SESSION_CHANGE_REFUSAL_CODES),\n message: isString,\n requestId: isOptional(isString),\n },\n usage_report: { report: isRecord },\n personal_usage_report: { requestId: isNonEmptyString, report: isRecord },\n personal_usage_report_error: {\n requestId: isNonEmptyString,\n code: oneOf(['not_available', 'report_failed']),\n message: isString,\n },\n stored_session_usage_report: {\n requestId: isNonEmptyString,\n sessionId: isNonEmptyString,\n report: isRecord,\n },\n stored_session_usage_report_error: {\n requestId: isNonEmptyString,\n sessionId: isNonEmptyString,\n code: oneOf(['not_available', 'report_failed']),\n message: isString,\n },\n executing: { executing: isBoolean },\n // An older host sends no count; a client then counts the one prompt it can see.\n pending: { pending: (v) => v === null || isString(v), pendingCount: isOptional(isCount) },\n execution_workspace_event: { snapshot: isRecord },\n execution_detail: { requestId: isNonEmptyString, page: isRecord },\n execution_detail_error: { requestId: isNonEmptyString, message: isString },\n waiting_loop_stop: { requestId: isNonEmptyString, outcome: isWaitingLoopStopOutcome },\n background_task_event: { event: isRecord },\n background_job_group_event: { event: isRecord },\n plan_event: { event: isRecord },\n context_file_refreshed: { event: isRecord },\n branch_event: { event: isRecord },\n background_tasks: { tasks: isRecordArray },\n background_task: { taskId: isString, task: isNullableRecord },\n background_job_groups: { groups: isRecordArray },\n background_job_group: { groupId: isString, group: isNullableRecord },\n background_task_log: { taskId: isString, page: isRecord },\n permission_request: { event: isRecord },\n ask_request: { event: isRecord },\n prompt_resolved: { event: isRecord },\n ui_intent: { event: isRecord },\n session_renamed: { event: isRecord },\n history_cleared: {},\n background_task_control_result: {\n action: oneOf(BACKGROUND_CONTROL_ACTIONS),\n taskId: isString,\n success: isBoolean,\n message: isOptional(isString),\n },\n protocol_error: { message: isString, requestId: isOptional(isString) },\n resume_gap: {},\n};\n\nfunction decodeVariant<TMessage extends { type: string }>(\n value: unknown,\n shapes: Readonly<Record<string, TVariantShape>>,\n reasonFor: (field: string) => string,\n): TMessageDecodeResult<TMessage> {\n if (!isRecord(value)) return { ok: false, reason: 'message must be a JSON object' };\n const type = value['type'];\n if (!isString(type)) return { ok: false, reason: 'message type must be a string' };\n const shape = Object.prototype.hasOwnProperty.call(shapes, type) ? shapes[type] : undefined;\n if (shape === undefined) return { ok: false, reason: `Unknown message type: ${type}` };\n for (const [field, check] of Object.entries(shape)) {\n if (!check(value[field])) return { ok: false, reason: reasonFor(field) };\n }\n return { ok: true, message: value as unknown as TMessage };\n}\n\n/** Total decode of a parsed client frame. */\nexport function decodeClientMessage(value: unknown): TMessageDecodeResult<TClientMessage> {\n return decodeVariant<TClientMessage>(value, CLIENT_MESSAGE_SHAPES, (field) =>\n field === 'prompt' ? 'prompt must be a non-empty string' : `${field} has an invalid shape`,\n );\n}\n\n/** Variant-level decode of a parsed server frame (foreign-owned payloads as records, see header). */\nexport function decodeServerMessage(value: unknown): TMessageDecodeResult<TServerMessage> {\n return decodeVariant<TServerMessage>(\n value,\n SERVER_MESSAGE_SHAPES,\n (field) => `${field} has an invalid shape`,\n );\n}\n\n/** `raw string → JSON → owner decoder`, the one path a carrier implements. */\nexport function decodeFrame<TMessage>(\n data: string,\n decode: (value: unknown) => TMessageDecodeResult<TMessage>,\n): TMessageDecodeResult<TMessage> {\n if (data.length > MAX_INBOUND_FRAME_BYTES) return { ok: false, reason: 'Frame too large' };\n let parsed: unknown;\n try {\n parsed = JSON.parse(data);\n // allow-fallback: the parse failure is returned as a typed refusal the carrier answers on the wire\n } catch {\n return { ok: false, reason: 'Invalid JSON' };\n }\n return decode(parsed);\n}\n","import type { TClientMessage } from './wire-messages.js';\n\n/**\n * The only inbound messages an observer may send: reads of this session. An allowlist, so a message\n * type added later is refused to observers until someone decides it is a read. `get-prompts` is left\n * out: an observer never receives prompts, open ones included.\n *\n * The host refuses anything else from an observer, and a client in observe mode sends only these, so\n * both sides read this one list.\n */\nexport const OBSERVER_MESSAGES: ReadonlySet<TClientMessage['type']> = new Set<\n TClientMessage['type']\n>([\n 'get-messages',\n 'get-history',\n 'get-context',\n 'get-status',\n 'get-commands',\n 'get-executing',\n 'get-pending',\n 'get-execution-workspace',\n 'read-execution-detail',\n 'get-usage-report',\n 'list-sessions',\n 'get-background-tasks',\n 'get-background-task',\n 'get-background-job-groups',\n 'get-background-job-group',\n 'wait-background-job-group',\n 'read-background-task-log',\n]);\n\n/** Whether an observer may send a message of this type. */\nexport function isObserverMessageType(type: string): boolean {\n return (OBSERVER_MESSAGES as ReadonlySet<string>).has(type);\n}\n"],"mappings":"mFAyBA,MAAa,EAA0B,EAAI,KAAO,KAIlD,SAAS,EAAS,EAAkC,CAClD,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,CAAK,CAC5E,CACA,MAAM,EAAY,GAA4B,OAAO,GAAM,SACrD,EAAoB,GAA4B,OAAO,GAAM,UAAY,EAAE,OAAS,EACpF,EAAa,GAA6B,OAAO,GAAM,UACvD,EAAkB,GAA4B,OAAO,GAAM,UAAY,OAAO,SAAS,CAAC,EACxF,EAAW,GAA4B,OAAO,cAAc,CAAC,GAAM,GAAgB,EACnF,EACH,GACA,GACC,IAAM,IAAA,IAAa,EAAM,CAAC,EACxB,EAAiB,GAAwB,MAAM,QAAQ,CAAC,GAAK,EAAE,MAAM,CAAQ,EAC7E,EAAoB,GAAwB,IAAM,MAAQ,EAAS,CAAC,EACpE,EACH,GACA,GACC,OAAO,GAAM,UAAY,EAAQ,SAAS,CAAC,EAEzC,EAEF,CAAE,KAAM,GAAM,eAAgB,GAAM,KAAM,GAAM,SAAU,EAAK,EAE7D,EAAsB,GAC1B,EAAS,CAAC,GACV,EAAS,EAAE,EAAK,GAChB,EAAS,EAAE,SAAY,GACvB,EAAS,EAAE,QAAW,GACtB,EAAS,EAAE,IAAO,EACd,EAAwB,GAC5B,MAAM,QAAQ,CAAC,GAAK,EAAE,MAAM,CAAkB,EAK1C,EAAwB,CAAC,QAAS,UAAW,WAAW,EACxD,EAAwB,CAAC,aAAc,YAAY,EACnD,EAA2B,CAC/B,SACA,UACA,qBACA,WACA,SACA,YACA,SACA,WACF,EACM,EAAkE,CAAC,SAAU,QAAS,MAAM,EAE5F,EAA2C,GAC/C,EAAS,CAAC,GACV,EAAW,EAAM,CAAqB,CAAC,CAAC,CAAC,EAAE,IAAO,GAClD,EAAW,EAAM,CAAwB,CAAC,CAAC,CAAC,EAAE,MAAS,GACvD,EAAW,EAAM,CAAqB,CAAC,CAAC,CAAC,EAAE,IAAO,GAClD,EAAW,CAAS,CAAC,CAAC,EAAE,aAAgB,EACpC,EAAsC,GAC1C,EAAS,CAAC,GAAK,EAAW,CAAQ,CAAC,CAAC,EAAE,MAAS,GAAK,EAAW,CAAQ,CAAC,CAAC,EAAE,KAAQ,EAC/E,EAA0C,GAAM,EAAS,CAAC,GAAK,EAAe,EAAE,MAAS,EACzF,EAAwC,GAAM,EAAS,CAAC,GAAK,EAAQ,EAAE,MAAS,EAChF,EAAwC,GAC5C,EAAU,CAAC,GAAK,IAAM,iBAAmB,IAAM,gBAC3C,EAAiC,GACrC,EAAS,CAAC,IACR,EAAE,OAAY,UACd,MAAM,QAAQ,EAAE,MAAS,GACzB,EAAE,OAAU,MAAM,CAAQ,GAC1B,EAAW,CAAQ,CAAC,CAAC,EAAE,IAAO,GAC7B,EAAE,OAAY,aAAe,EAAE,SAAc,IAAA,IAAa,EAAE,OAAY,IAAA,IAIvE,EAEF,CACF,KAAM,CAAC,EACP,QAAS,CAAE,QAAS,CAAS,EAC7B,QAAS,CAAE,OAAQ,EAAkB,QAAS,EAAW,CAAQ,CAAE,EACnE,OAAQ,CAAE,OAAQ,EAAW,CAAgB,EAAG,QAAS,CAAS,CACpE,EACM,EAAyC,GAAM,CACnD,GAAI,CAAC,EAAS,CAAC,GAAK,CAAC,EAAS,EAAE,IAAO,EAAG,MAAO,GACjD,IAAM,EAAO,EAAE,KACf,GAAI,CAAC,OAAO,UAAU,eAAe,KAAK,EAAkC,CAAI,EAAG,MAAO,GAC1F,IAAM,EAAQ,EAAiC,GAC/C,OAAO,OAAO,QAAQ,CAAK,CAAC,CAAC,OAAO,CAAC,EAAO,KAAW,EAAM,EAAE,EAAM,CAAC,CACxE,EAGa,EAAiF,CAC5F,OAAQ,CAAE,OAAQ,CAAiB,EACnC,QAAS,CACP,KAAM,EACN,KAAM,EAAW,CAAQ,EACzB,UAAW,EAAW,CAAgB,CACxC,EACA,MAAO,CAAC,EACR,eAAgB,CAAC,EACjB,eAAgB,CAAC,EACjB,cAAe,CAAE,UAAW,EAAW,CAAO,CAAE,EAChD,cAAe,CAAC,EAChB,cAAe,CAAC,EAChB,eAAgB,CAAC,EACjB,aAAc,CAAC,EACf,gBAAiB,CAAE,UAAW,CAAiB,EAC/C,cAAe,CAAE,UAAW,EAAW,CAAgB,CAAE,EACzD,iBAAkB,CAAE,UAAW,EAAkB,UAAW,EAAW,CAAgB,CAAE,EACzF,mBAAoB,CAAC,EACrB,4BAA6B,CAC3B,UAAW,EACX,OAAQ,EAAM,CAAC,KAAM,KAAK,CAAC,EAC3B,SAAU,CACZ,EACA,kCAAmC,CACjC,UAAW,EACX,UAAW,CACb,EACA,gBAAiB,CAAC,EAClB,cAAe,CAAC,EAChB,0BAA2B,CAAC,EAC5B,wBAAyB,CACvB,UAAW,EACX,QAAS,EACT,OAAQ,EAAW,CAAuB,CAC5C,EACA,oBAAqB,CAAE,UAAW,CAAiB,EACnD,uBAAwB,CAAE,OAAQ,EAAW,CAA0B,CAAE,EACzE,sBAAuB,CAAE,OAAQ,CAAiB,EAClD,4BAA6B,CAAC,EAC9B,2BAA4B,CAAE,QAAS,CAAiB,EACxD,4BAA6B,CAAE,QAAS,CAAiB,EACzD,yBAA0B,CAAE,OAAQ,EAAkB,OAAQ,EAAW,CAAQ,CAAE,EACnF,wBAAyB,CAAE,OAAQ,CAAiB,EACpD,uBAAwB,CAAE,OAAQ,EAAkB,MAAO,CAAsB,EACjF,2BAA4B,CAC1B,OAAQ,EACR,OAAQ,EAAW,CAAyB,CAC9C,EACA,sBAAuB,CAAE,GAAI,EAAkB,OAAQ,CAAwB,EAC/E,eAAgB,CAAE,GAAI,EAAkB,SAAU,CAAiB,EACnE,OAAQ,CAAE,QAAS,CAAe,EAClC,IAAK,CAAE,IAAK,CAAe,CAC7B,EAEM,EAA0B,CAAE,SAAU,EAAW,CAAQ,CAAE,EAGpD,EAAiF,CAC5F,WAAY,CAAE,MAAO,EAAU,GAAG,CAAS,EAC3C,aAAc,CAAE,QAAS,EAAU,GAAG,CAAS,EAC/C,WAAY,CAAE,MAAO,EAAU,GAAG,CAAS,EAC3C,SAAU,CAAE,MAAO,EAAU,GAAG,CAAS,EACzC,SAAU,CAAE,WAAY,EAAW,GAAG,CAAS,EAC/C,SAAU,CAAE,OAAQ,EAAU,GAAG,CAAS,EAC1C,YAAa,CAAE,OAAQ,EAAU,GAAG,CAAS,EAC7C,MAAO,CAAE,QAAS,EAAU,GAAG,CAAS,EACxC,eAAgB,CACd,KAAM,EACN,QAAS,EACT,QAAS,EACT,UAAW,EAAW,CAAQ,CAChC,EACA,SAAU,CAAE,SAAU,CAAc,EACpC,QAAS,CAAE,WAAY,EAAS,MAAO,EAAS,QAAS,CAAqB,EAC9E,QAAS,CAAE,MAAO,CAAS,EAC3B,gBAAiB,CAAC,EAClB,YAAa,CAAE,OAAQ,EAAM,OAAO,KAAK,CAAY,CAAC,CAAE,EACxD,SAAU,CAAE,SAAU,EAAe,OAAQ,CAAc,EAC3D,eAAgB,CAAE,OAAQ,CAAS,EACnC,SAAU,CAAE,UAAW,EAAkB,QAAS,CAAS,EAC3D,eAAgB,CACd,UAAW,EACX,KAAM,EAAM,CAAC,gBAAiB,aAAa,CAAC,EAC5C,QAAS,CACX,EACA,iBAAkB,CAAE,MAAO,CAAS,EACpC,sBAAuB,CACrB,KAAM,EAAM,CAA4B,EACxC,QAAS,EACT,UAAW,EAAW,CAAQ,CAChC,EACA,aAAc,CAAE,OAAQ,CAAS,EACjC,sBAAuB,CAAE,UAAW,EAAkB,OAAQ,CAAS,EACvE,4BAA6B,CAC3B,UAAW,EACX,KAAM,EAAM,CAAC,gBAAiB,eAAe,CAAC,EAC9C,QAAS,CACX,EACA,4BAA6B,CAC3B,UAAW,EACX,UAAW,EACX,OAAQ,CACV,EACA,kCAAmC,CACjC,UAAW,EACX,UAAW,EACX,KAAM,EAAM,CAAC,gBAAiB,eAAe,CAAC,EAC9C,QAAS,CACX,EACA,UAAW,CAAE,UAAW,CAAU,EAElC,QAAS,CAAE,QAAU,GAAM,IAAM,MAAQ,EAAS,CAAC,EAAG,aAAc,EAAW,CAAO,CAAE,EACxF,0BAA2B,CAAE,SAAU,CAAS,EAChD,iBAAkB,CAAE,UAAW,EAAkB,KAAM,CAAS,EAChE,uBAAwB,CAAE,UAAW,EAAkB,QAAS,CAAS,EACzE,kBAAmB,CAAE,UAAW,EAAkB,QAAS,CAAyB,EACpF,sBAAuB,CAAE,MAAO,CAAS,EACzC,2BAA4B,CAAE,MAAO,CAAS,EAC9C,WAAY,CAAE,MAAO,CAAS,EAC9B,uBAAwB,CAAE,MAAO,CAAS,EAC1C,aAAc,CAAE,MAAO,CAAS,EAChC,iBAAkB,CAAE,MAAO,CAAc,EACzC,gBAAiB,CAAE,OAAQ,EAAU,KAAM,CAAiB,EAC5D,sBAAuB,CAAE,OAAQ,CAAc,EAC/C,qBAAsB,CAAE,QAAS,EAAU,MAAO,CAAiB,EACnE,oBAAqB,CAAE,OAAQ,EAAU,KAAM,CAAS,EACxD,mBAAoB,CAAE,MAAO,CAAS,EACtC,YAAa,CAAE,MAAO,CAAS,EAC/B,gBAAiB,CAAE,MAAO,CAAS,EACnC,UAAW,CAAE,MAAO,CAAS,EAC7B,gBAAiB,CAAE,MAAO,CAAS,EACnC,gBAAiB,CAAC,EAClB,+BAAgC,CAC9B,OAAQ,EAAM,CAA0B,EACxC,OAAQ,EACR,QAAS,EACT,QAAS,EAAW,CAAQ,CAC9B,EACA,eAAgB,CAAE,QAAS,EAAU,UAAW,EAAW,CAAQ,CAAE,EACrE,WAAY,CAAC,CACf,EAEA,SAAS,EACP,EACA,EACA,EACgC,CAChC,GAAI,CAAC,EAAS,CAAK,EAAG,MAAO,CAAE,GAAI,GAAO,OAAQ,+BAAgC,EAClF,IAAM,EAAO,EAAM,KACnB,GAAI,CAAC,EAAS,CAAI,EAAG,MAAO,CAAE,GAAI,GAAO,OAAQ,+BAAgC,EACjF,IAAM,EAAQ,OAAO,UAAU,eAAe,KAAK,EAAQ,CAAI,EAAI,EAAO,GAAQ,IAAA,GAClF,GAAI,IAAU,IAAA,GAAW,MAAO,CAAE,GAAI,GAAO,OAAQ,yBAAyB,GAAO,EACrF,IAAK,GAAM,CAAC,EAAO,KAAU,OAAO,QAAQ,CAAK,EAC/C,GAAI,CAAC,EAAM,EAAM,EAAM,EAAG,MAAO,CAAE,GAAI,GAAO,OAAQ,EAAU,CAAK,CAAE,EAEzE,MAAO,CAAE,GAAI,GAAM,QAAS,CAA6B,CAC3D,CAGA,SAAgB,EAAoB,EAAsD,CACxF,OAAO,EAA8B,EAAO,EAAwB,GAClE,IAAU,SAAW,oCAAsC,GAAG,EAAM,sBACtE,CACF,CAGA,SAAgB,EAAoB,EAAsD,CACxF,OAAO,EACL,EACA,EACC,GAAU,GAAG,EAAM,sBACtB,CACF,CAGA,SAAgB,EACd,EACA,EACgC,CAChC,GAAI,EAAK,OAAA,QAAkC,MAAO,CAAE,GAAI,GAAO,OAAQ,iBAAkB,EACzF,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,CAAI,CAE1B,MAAQ,CACN,MAAO,CAAE,GAAI,GAAO,OAAQ,cAAe,CAC7C,CACA,OAAO,EAAO,CAAM,CACtB,CCxSA,MAAa,EAAyD,IAAI,IAExE,CACA,eACA,cACA,cACA,aACA,eACA,gBACA,cACA,0BACA,wBACA,mBACA,gBACA,uBACA,sBACA,4BACA,2BACA,4BACA,0BACF,CAAC,EAGD,SAAgB,EAAsB,EAAuB,CAC3D,OAAQ,EAA0C,IAAI,CAAI,CAC5D"}
|