@robota-sdk/agent-transport 3.0.0-beta.83 → 3.0.0-beta.85
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 +62 -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__/handoff-chunking.test.ts +44 -1
- 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/handoff-chunking.ts +52 -12
- 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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the `error` wire frame should say about a session failure, beyond its raw `message` (#3289
|
|
3
|
+
* §3). A renderer maps `code` to one plain sentence with the next step, keeping `message` behind a
|
|
4
|
+
* "Details" disclosure — see `agent-ui-web`'s `SessionNotices`.
|
|
5
|
+
*
|
|
6
|
+
* Walks wrapped layers (`originalError`/`cause`, a few deep) because a higher-level catch can wrap the
|
|
7
|
+
* provider's own typed error in another one (e.g. a "Conversation failed" `ProviderError`) before it
|
|
8
|
+
* reaches here — the same reason `agent-core`'s `classifyProviderFailure` walks layers rather than
|
|
9
|
+
* looking at the outermost error alone.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
ModelNotAvailableError,
|
|
15
|
+
NetworkError,
|
|
16
|
+
ProviderError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
} from '@robota-sdk/agent-core';
|
|
19
|
+
|
|
20
|
+
/** Kept in sync with `wire-messages.ts`'s `error` variant — see that type for what each means. */
|
|
21
|
+
export type TSessionErrorCode =
|
|
22
|
+
'auth' | 'rate_limit' | 'model_unavailable' | 'network' | 'provider';
|
|
23
|
+
|
|
24
|
+
export interface ISessionErrorClassification {
|
|
25
|
+
code?: TSessionErrorCode;
|
|
26
|
+
provider?: string;
|
|
27
|
+
retryAfterSeconds?: number;
|
|
28
|
+
/** For `code: 'model_unavailable'`, the model the request tried, when the failure named one. */
|
|
29
|
+
model?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const MAX_WRAP_DEPTH = 8;
|
|
33
|
+
|
|
34
|
+
/** The error and everything it wraps, outermost first, followed to a small depth and never twice. */
|
|
35
|
+
function layersOf(error: unknown): unknown[] {
|
|
36
|
+
const layers: unknown[] = [];
|
|
37
|
+
const seen = new Set<unknown>();
|
|
38
|
+
let frontier: unknown[] = [error];
|
|
39
|
+
for (let depth = 0; depth <= MAX_WRAP_DEPTH && frontier.length > 0; depth++) {
|
|
40
|
+
const next: unknown[] = [];
|
|
41
|
+
for (const layer of frontier) {
|
|
42
|
+
if (layer === undefined || layer === null || seen.has(layer)) continue;
|
|
43
|
+
seen.add(layer);
|
|
44
|
+
layers.push(layer);
|
|
45
|
+
if (typeof layer === 'object') {
|
|
46
|
+
const record = layer as Record<string, unknown>;
|
|
47
|
+
next.push(record['originalError'], record['cause']);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
frontier = next;
|
|
51
|
+
}
|
|
52
|
+
return layers;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Classify a session failure for the wire, or leave it uncoded when it names no provider at all —
|
|
57
|
+
* an unrelated internal failure keeps showing its raw `message` exactly as before this classification
|
|
58
|
+
* existed.
|
|
59
|
+
*/
|
|
60
|
+
export function classifySessionErrorForWire(error: unknown): ISessionErrorClassification {
|
|
61
|
+
// A generic ProviderError never wins outright — a wrapper (e.g. "Conversation failed") can sit
|
|
62
|
+
// above the real, more specific cause, so it is kept only as a fallback while deeper layers are
|
|
63
|
+
// still searched for one of the specific types below.
|
|
64
|
+
let fallback: ISessionErrorClassification | undefined;
|
|
65
|
+
for (const layer of layersOf(error)) {
|
|
66
|
+
if (layer instanceof AuthenticationError) {
|
|
67
|
+
return { code: 'auth', ...(layer.provider !== undefined && { provider: layer.provider }) };
|
|
68
|
+
}
|
|
69
|
+
if (layer instanceof RateLimitError) {
|
|
70
|
+
return {
|
|
71
|
+
code: 'rate_limit',
|
|
72
|
+
...(layer.provider !== undefined && { provider: layer.provider }),
|
|
73
|
+
...(layer.retryAfter !== undefined && { retryAfterSeconds: layer.retryAfter }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (layer instanceof ModelNotAvailableError) {
|
|
77
|
+
return {
|
|
78
|
+
code: 'model_unavailable',
|
|
79
|
+
provider: layer.provider,
|
|
80
|
+
...(layer.model !== undefined && { model: layer.model }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (layer instanceof NetworkError) {
|
|
84
|
+
return { code: 'network', ...(layer.provider !== undefined && { provider: layer.provider }) };
|
|
85
|
+
}
|
|
86
|
+
if (fallback === undefined && layer instanceof ProviderError) {
|
|
87
|
+
fallback = { code: 'provider', provider: layer.provider };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return fallback ?? {};
|
|
91
|
+
}
|
package/src/session-events.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { holdOpenPrompts } from './open-prompts.js';
|
|
10
|
+
import { classifySessionErrorForWire } from './session-error-classification.js';
|
|
10
11
|
|
|
11
12
|
import type { TOutboundDeliver } from './outbound-delivery.js';
|
|
12
13
|
import type { IProtocolSession } from './protocol-session.js';
|
|
@@ -131,7 +132,12 @@ export function subscribeSessionEvents(
|
|
|
131
132
|
const onInterrupted = (result: IExecutionResult): void =>
|
|
132
133
|
deliver({ type: 'interrupted', result: withoutHistory(result), ...attr() });
|
|
133
134
|
const onError = (error: Error): void =>
|
|
134
|
-
deliver({
|
|
135
|
+
deliver({
|
|
136
|
+
type: 'error',
|
|
137
|
+
message: error.message,
|
|
138
|
+
...classifySessionErrorForWire(error),
|
|
139
|
+
...attr(),
|
|
140
|
+
});
|
|
135
141
|
const onBackgroundTaskEvent = (event: TBackgroundTaskEvent): void =>
|
|
136
142
|
deliver({ type: 'background_task_event', event });
|
|
137
143
|
const onBackgroundJobGroupEvent = (event: TBackgroundJobGroupEvent): void =>
|
|
@@ -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
|
+
}
|