@robota-sdk/agent-transport 3.0.0-beta.100

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 (93) hide show
  1. package/CHANGELOG.md +1137 -0
  2. package/LICENSE +661 -0
  3. package/README.md +121 -0
  4. package/dist/browser/client.d.ts +2 -0
  5. package/dist/browser/client.js +1 -0
  6. package/dist/browser/index.d.ts +624 -0
  7. package/dist/browser/index.d.ts.map +1 -0
  8. package/dist/browser/index.js +2 -0
  9. package/dist/browser/index.js.map +1 -0
  10. package/dist/browser/observer-messages-4fRQZU2D.d.ts +499 -0
  11. package/dist/browser/observer-messages-4fRQZU2D.d.ts.map +1 -0
  12. package/dist/browser/observer-messages-O8jTPEQO.js +2 -0
  13. package/dist/browser/observer-messages-O8jTPEQO.js.map +1 -0
  14. package/dist/node/index.cjs +1 -0
  15. package/dist/node/index.d.cts +1115 -0
  16. package/dist/node/index.d.cts.map +1 -0
  17. package/dist/node/index.d.ts +1115 -0
  18. package/dist/node/index.d.ts.map +1 -0
  19. package/dist/node/index.js +2 -0
  20. package/dist/node/index.js.map +1 -0
  21. package/dist/node/node.cjs +1 -0
  22. package/dist/node/node.d.cts +317 -0
  23. package/dist/node/node.d.cts.map +1 -0
  24. package/dist/node/node.d.ts +317 -0
  25. package/dist/node/node.d.ts.map +1 -0
  26. package/dist/node/node.js +2 -0
  27. package/dist/node/node.js.map +1 -0
  28. package/package.json +122 -0
  29. package/src/__tests__/__fixtures__/korean-text.json +6 -0
  30. package/src/__tests__/access-token-verifier.test.ts +672 -0
  31. package/src/__tests__/admission.test.ts +130 -0
  32. package/src/__tests__/app-tool-messages.test.ts +463 -0
  33. package/src/__tests__/bearer-resource-gate.test.ts +120 -0
  34. package/src/__tests__/channel-frames.test.ts +215 -0
  35. package/src/__tests__/driver-attribution-capability.test.ts +83 -0
  36. package/src/__tests__/file-transfer.test.ts +281 -0
  37. package/src/__tests__/handoff-chunking.test.ts +261 -0
  38. package/src/__tests__/handoff-manifest.test.ts +52 -0
  39. package/src/__tests__/history-attached-budget.test.ts +216 -0
  40. package/src/__tests__/message-decoders.test.ts +870 -0
  41. package/src/__tests__/outbound-delivery.test.ts +558 -0
  42. package/src/__tests__/peer-message-ledger.test.ts +200 -0
  43. package/src/__tests__/permission-summary.test.ts +18 -0
  44. package/src/__tests__/personal-usage-report.test.ts +193 -0
  45. package/src/__tests__/project-read-messages.test.ts +118 -0
  46. package/src/__tests__/resume-buffer.test.ts +93 -0
  47. package/src/__tests__/session-detail-loop-status.test.ts +169 -0
  48. package/src/__tests__/session-directory-messages.test.ts +322 -0
  49. package/src/__tests__/session-error-classification.test.ts +113 -0
  50. package/src/__tests__/session-event-delivery.test.ts +101 -0
  51. package/src/__tests__/session-history-wire.test.ts +364 -0
  52. package/src/__tests__/session-message-handler-e5.test.ts +99 -0
  53. package/src/__tests__/session-message-handler-observe.test.ts +223 -0
  54. package/src/__tests__/session-message-handler.test.ts +726 -0
  55. package/src/__tests__/session-resume-bridge.test.ts +472 -0
  56. package/src/__tests__/settings-messages.test.ts +193 -0
  57. package/src/__tests__/stdio-carrier.test.ts +85 -0
  58. package/src/__tests__/submit-prompt-shape.test.ts +78 -0
  59. package/src/__tests__/usage-report-carrier.test.ts +98 -0
  60. package/src/__tests__/ws-broadcast-events.test.ts +94 -0
  61. package/src/app-tool-messages.ts +140 -0
  62. package/src/background-messages.ts +169 -0
  63. package/src/channel-frames.ts +182 -0
  64. package/src/client.ts +17 -0
  65. package/src/handoff-chunking.ts +275 -0
  66. package/src/index.ts +135 -0
  67. package/src/loop-control-messages.ts +34 -0
  68. package/src/message-decoders.ts +485 -0
  69. package/src/message-parser.ts +33 -0
  70. package/src/node/access-token-verifier.ts +363 -0
  71. package/src/node/admission.ts +153 -0
  72. package/src/node/bearer-resource-gate.ts +331 -0
  73. package/src/node/file-transfer.ts +530 -0
  74. package/src/node/handoff-manifest.ts +77 -0
  75. package/src/node/index.ts +45 -0
  76. package/src/node/stdio-carrier.ts +59 -0
  77. package/src/observer-messages.ts +47 -0
  78. package/src/open-prompts.ts +76 -0
  79. package/src/outbound-delivery.ts +240 -0
  80. package/src/peer-message-ledger.ts +193 -0
  81. package/src/permission-summary.ts +18 -0
  82. package/src/project-read-messages.ts +74 -0
  83. package/src/protocol-session.ts +56 -0
  84. package/src/resume-buffer.ts +123 -0
  85. package/src/session-directory-messages.ts +162 -0
  86. package/src/session-error-classification.ts +91 -0
  87. package/src/session-events.ts +290 -0
  88. package/src/session-message-handler.ts +413 -0
  89. package/src/session-query-messages.ts +160 -0
  90. package/src/session-resume-bridge.ts +275 -0
  91. package/src/settings-messages.ts +103 -0
  92. package/src/usage-messages.ts +195 -0
  93. package/src/wire-messages.ts +436 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Outbound session-event subscription for the WS/WebRTC transports (split from `session-message-handler.ts` —
3
+ * the inbound message routing stays there; this module owns the session→`TServerMessage` fan-out).
4
+ *
5
+ * Used by `createSessionMessageHandler` (one subscription per channel) and by the persistent
6
+ * `SessionResumeBridge` (REMOTE-013 E4 — a SINGLE subscription that outlives per-channel handlers).
7
+ */
8
+
9
+ import { holdOpenPrompts } from './open-prompts.js';
10
+ import { classifySessionErrorForWire } from './session-error-classification.js';
11
+
12
+ import type { TOutboundDeliver } from './outbound-delivery.js';
13
+ import type { IProtocolSession } from './protocol-session.js';
14
+ import type { TWireExecutionResult } from './wire-messages.js';
15
+ import type {
16
+ IExecutionWorkspaceEvent,
17
+ TBackgroundJobGroupEvent,
18
+ TBackgroundTaskEvent,
19
+ } from '@robota-sdk/agent-interface-execution';
20
+ import type {
21
+ TDriverId,
22
+ TInteractiveEventName,
23
+ TTurnSource,
24
+ } from '@robota-sdk/agent-interface-session';
25
+ import type {
26
+ IAskRequestEvent,
27
+ IBranchEvent,
28
+ ICompactEvent,
29
+ IContextFileRefreshedEvent,
30
+ IExecutionResult,
31
+ IPermissionRequestEvent,
32
+ IPromptResolvedEvent,
33
+ IPlanApprovalEvent,
34
+ ISessionRenamedEvent,
35
+ ISessionStatusSnapshot,
36
+ ISessionSwitchedEvent,
37
+ IToolState,
38
+ IUiIntentEvent,
39
+ } from '@robota-sdk/agent-interface-session';
40
+
41
+ export type TProtocolSessionEventClassification = 'forwarded' | 'requester-routed' | 'non-surface';
42
+
43
+ /** Exhaustive protocol-owner policy over the shared session-event vocabulary. */
44
+ export const PROTOCOL_SESSION_EVENT_CLASSIFICATION = {
45
+ text_delta: 'forwarded',
46
+ tool_start: 'forwarded',
47
+ tool_end: 'forwarded',
48
+ thinking: 'forwarded',
49
+ thinking_delta: 'non-surface',
50
+ complete: 'forwarded',
51
+ error: 'forwarded',
52
+ context_update: 'forwarded',
53
+ compact: 'forwarded',
54
+ interrupted: 'forwarded',
55
+ skill_activation: 'forwarded',
56
+ background_task_event: 'forwarded',
57
+ background_job_group_event: 'forwarded',
58
+ execution_workspace_event: 'forwarded',
59
+ user_message: 'forwarded',
60
+ turn_source: 'forwarded',
61
+ context_file_refreshed: 'forwarded',
62
+ memory_event: 'forwarded',
63
+ goal_event: 'non-surface',
64
+ plan_event: 'forwarded',
65
+ branch_event: 'forwarded',
66
+ permission_request: 'forwarded',
67
+ ask_request: 'forwarded',
68
+ prompt_resolved: 'forwarded',
69
+ ui_intent: 'requester-routed',
70
+ session_renamed: 'forwarded',
71
+ history_cleared: 'forwarded',
72
+ session_switched: 'forwarded',
73
+ status_changed: 'forwarded',
74
+ } as const satisfies Record<TInteractiveEventName, TProtocolSessionEventClassification>;
75
+
76
+ /**
77
+ * Options for {@link subscribeSessionEvents}.
78
+ *
79
+ * ARCH-030: `onDeliveryError` is NOT here. Carrier-failure containment moved to the one
80
+ * connection-scoped {@link TOutboundDeliver} boundary this function now receives, so the fan-out and
81
+ * every reply share a single guard instead of the fan-out having its own.
82
+ */
83
+ export interface ISubscribeSessionEventsOptions {
84
+ /**
85
+ * CMD-004 Stage D: lazily read the SERVER-ASSIGNED driver id of THIS surface — `ui_intent` is
86
+ * requester-routed against it (lazy because the resume bridge binds the id only after pairing).
87
+ */
88
+ getSurfaceDriverId?: () => TDriverId | undefined;
89
+ /**
90
+ * `false` leaves `permission_request` and `ask_request` unsubscribed. The session parks a prompt
91
+ * only while someone listens for it, so a surface that may not answer must not listen: otherwise an
92
+ * unattended session would hold a prompt nobody can settle instead of failing it closed at once.
93
+ * Defaults to `true`.
94
+ */
95
+ receivePrompts?: boolean;
96
+ }
97
+
98
+ /**
99
+ * Subscribe the session's events and forward each as a `TServerMessage` through the connection's
100
+ * outbound boundary; returns an unsubscribe. Exported (REMOTE-013 E4) so the persistent
101
+ * `SessionResumeBridge` can own a SINGLE subscription that outlives per-channel handlers.
102
+ *
103
+ * ARCH-030: the second parameter is the boundary, not a raw sink — a carrier `send` cannot be passed
104
+ * here, which is what keeps the fan-out and the reply paths on one guard.
105
+ */
106
+ export function subscribeSessionEvents(
107
+ session: IProtocolSession,
108
+ deliver: TOutboundDeliver,
109
+ options: ISubscribeSessionEventsOptions = {},
110
+ ): () => void {
111
+ // ARCH-030: no local guard wrapper any more, and no event name passed alongside the message. The name
112
+ // existed so the local guard could label a failure with it; the boundary labels with `message.type`,
113
+ // the frame that could not be delivered. Keeping the argument would have been a parameter that
114
+ // documents nothing and is read by nobody.
115
+ // REMOTE-014 E5: stamp the ACTIVE turn's driver id onto TURN-AUTHORED events (co-drive authorship,
116
+ // display-only), read at emit time. Only these events — background/goal/memory/execution-workspace events
117
+ // are NOT authored by a driver turn and carry no `driverId`. `undefined` when idle or unattributed.
118
+ const attr = (): { driverId?: TDriverId } => {
119
+ // ARCH-012: a plain call, not `?.() ?? undefined`. The member is required, so `null` means
120
+ // nobody is driving — it can no longer also mean "this host cannot answer".
121
+ const driverId = session.getActiveDriverId();
122
+ return driverId ? { driverId } : {};
123
+ };
124
+ const onUserMessage = (content: string): void =>
125
+ deliver({ type: 'user_message', content, ...attr() });
126
+ const onTextDelta = (delta: string): void => deliver({ type: 'text_delta', delta, ...attr() });
127
+ const onToolStart = (state: IToolState): void =>
128
+ deliver({ type: 'tool_start', state, ...attr() });
129
+ const onToolEnd = (state: IToolState): void => deliver({ type: 'tool_end', state, ...attr() });
130
+ const onThinking = (isThinking: boolean): void =>
131
+ deliver({ type: 'thinking', isThinking, ...attr() });
132
+ const onComplete = (result: IExecutionResult): void =>
133
+ deliver({ type: 'complete', result: withoutHistory(result), ...attr() });
134
+ const onInterrupted = (result: IExecutionResult): void =>
135
+ deliver({ type: 'interrupted', result: withoutHistory(result), ...attr() });
136
+ const onError = (error: Error): void =>
137
+ deliver({
138
+ type: 'error',
139
+ message: error.message,
140
+ ...classifySessionErrorForWire(error),
141
+ ...attr(),
142
+ });
143
+ const onBackgroundTaskEvent = (event: TBackgroundTaskEvent): void =>
144
+ deliver({ type: 'background_task_event', event });
145
+ const onBackgroundJobGroupEvent = (event: TBackgroundJobGroupEvent): void =>
146
+ deliver({ type: 'background_job_group_event', event });
147
+ const onExecutionWorkspace = (event: IExecutionWorkspaceEvent): void =>
148
+ deliver({
149
+ type: 'execution_workspace_event',
150
+ snapshot: event.snapshot,
151
+ });
152
+ const onPlanEvent = (event: IPlanApprovalEvent): void => deliver({ type: 'plan_event', event });
153
+ const onContextFileRefreshed = (event: IContextFileRefreshedEvent): void =>
154
+ deliver({ type: 'context_file_refreshed', event });
155
+ const onBranchEvent = (event: IBranchEvent): void => deliver({ type: 'branch_event', event });
156
+ // REMOTE-007: forward the transport-neutral prompt events so a remote surface can render + answer the
157
+ // SAME permission/ask prompt; `prompt_resolved` dismisses it when another surface answered first.
158
+ // #3189: kept while open, so a client that attaches later can ask for it with `get-prompts`.
159
+ const receivePrompts = options.receivePrompts ?? true;
160
+ const openPrompts = receivePrompts ? holdOpenPrompts(session) : undefined;
161
+ const onPermissionRequest = (event: IPermissionRequestEvent): void => {
162
+ const frame = { type: 'permission_request', event } as const;
163
+ openPrompts?.record(frame);
164
+ deliver(frame);
165
+ };
166
+ const onAskRequest = (event: IAskRequestEvent): void => {
167
+ const frame = { type: 'ask_request', event } as const;
168
+ openPrompts?.record(frame);
169
+ deliver(frame);
170
+ };
171
+ const onPromptResolved = (event: IPromptResolvedEvent): void => {
172
+ openPrompts?.forget(event.id);
173
+ deliver({ type: 'prompt_resolved', event });
174
+ };
175
+ // CMD-004 Stage D: `ui_intent` is REQUESTER-ROUTED — delivered only to the surface whose
176
+ // server-assigned driver id issued the command. An UNATTRIBUTED intent (no requester id, e.g. an
177
+ // idle model-invoked command) is unroutable and reaches every surface — never a silent drop.
178
+ const onUiIntent = (event: IUiIntentEvent): void => {
179
+ if (
180
+ event.requesterDriverId !== undefined &&
181
+ event.requesterDriverId !== options.getSurfaceDriverId?.()
182
+ ) {
183
+ return; // another surface's intent — this surface never sees it (and never buffers it)
184
+ }
185
+ deliver({ type: 'ui_intent', event });
186
+ };
187
+ // CMD-004 Stage E: BROADCAST session-state events — the host executed the rename/clear; EVERY
188
+ // attached surface (co-driving included) reflects it. Never requester-filtered, unlike `ui_intent`.
189
+ const onSessionRenamed = (event: ISessionRenamedEvent): void =>
190
+ deliver({ type: 'session_renamed', event });
191
+ const onHistoryCleared = (): void => deliver({ type: 'history_cleared' });
192
+ // #3189: this surface's session is now another one; it re-reads what it shows. Only surfaces
193
+ // subscribed to the session that moved hear it: a host that binds each connection to its own session
194
+ // sends it to the connection that switched, never to the others.
195
+ const onSessionSwitched = (event: ISessionSwitchedEvent): void => {
196
+ // This connection no longer answers the previous session's prompts; it reads the new session's anew.
197
+ openPrompts?.clear();
198
+ deliver({ type: 'session_switched', event });
199
+ };
200
+ // #3189: what a client that renders the whole session (the TUI) needs beside the streamed turn.
201
+ // The context window is pushed in the frame `get-context` answers with. A compaction, a skill
202
+ // activation or a memory event adds history entries no streamed frame carries, so the client is
203
+ // told the history changed and re-reads it with `get-history`; the entries themselves are not sent.
204
+ const onContextUpdate = (state: ReturnType<IProtocolSession['getContextState']>): void =>
205
+ deliver({ type: 'context', state });
206
+ const onHistoryChanged = (): void => deliver({ type: 'history_changed' });
207
+ const onCompact = (event: ICompactEvent): void =>
208
+ deliver({
209
+ type: 'history_changed',
210
+ compact: {
211
+ trigger: event.trigger,
212
+ beforeUsedPercentage: event.before.usedPercentage,
213
+ afterUsedPercentage: event.after.usedPercentage,
214
+ },
215
+ });
216
+ const onTurnSource = (source: TTurnSource): void => deliver({ type: 'turn_source', source });
217
+ // One client's `/mode` or `/model` shows on every client on this session, in the frame
218
+ // `get-status` answers with.
219
+ const onStatusChanged = (status: ISessionStatusSnapshot): void =>
220
+ deliver({ type: 'session_status', status });
221
+
222
+ session.on('user_message', onUserMessage);
223
+ session.on('text_delta', onTextDelta);
224
+ session.on('tool_start', onToolStart);
225
+ session.on('tool_end', onToolEnd);
226
+ session.on('thinking', onThinking);
227
+ session.on('complete', onComplete);
228
+ session.on('interrupted', onInterrupted);
229
+ session.on('error', onError);
230
+ session.on('background_task_event', onBackgroundTaskEvent);
231
+ session.on('background_job_group_event', onBackgroundJobGroupEvent);
232
+ session.on('execution_workspace_event', onExecutionWorkspace);
233
+ session.on('plan_event', onPlanEvent);
234
+ session.on('context_file_refreshed', onContextFileRefreshed);
235
+ session.on('branch_event', onBranchEvent);
236
+ if (receivePrompts) {
237
+ session.on('permission_request', onPermissionRequest);
238
+ session.on('ask_request', onAskRequest);
239
+ }
240
+ session.on('prompt_resolved', onPromptResolved);
241
+ session.on('ui_intent', onUiIntent);
242
+ session.on('session_renamed', onSessionRenamed);
243
+ session.on('history_cleared', onHistoryCleared);
244
+ session.on('session_switched', onSessionSwitched);
245
+ session.on('context_update', onContextUpdate);
246
+ session.on('compact', onCompact);
247
+ session.on('skill_activation', onHistoryChanged);
248
+ session.on('memory_event', onHistoryChanged);
249
+ session.on('turn_source', onTurnSource);
250
+ session.on('status_changed', onStatusChanged);
251
+
252
+ return (): void => {
253
+ session.off('user_message', onUserMessage);
254
+ session.off('text_delta', onTextDelta);
255
+ session.off('tool_start', onToolStart);
256
+ session.off('tool_end', onToolEnd);
257
+ session.off('thinking', onThinking);
258
+ session.off('complete', onComplete);
259
+ session.off('interrupted', onInterrupted);
260
+ session.off('error', onError);
261
+ session.off('background_task_event', onBackgroundTaskEvent);
262
+ session.off('background_job_group_event', onBackgroundJobGroupEvent);
263
+ session.off('execution_workspace_event', onExecutionWorkspace);
264
+ session.off('plan_event', onPlanEvent);
265
+ session.off('context_file_refreshed', onContextFileRefreshed);
266
+ session.off('branch_event', onBranchEvent);
267
+ if (receivePrompts) {
268
+ session.off('permission_request', onPermissionRequest);
269
+ session.off('ask_request', onAskRequest);
270
+ }
271
+ session.off('prompt_resolved', onPromptResolved);
272
+ session.off('ui_intent', onUiIntent);
273
+ session.off('session_renamed', onSessionRenamed);
274
+ session.off('history_cleared', onHistoryCleared);
275
+ session.off('session_switched', onSessionSwitched);
276
+ session.off('context_update', onContextUpdate);
277
+ session.off('compact', onCompact);
278
+ session.off('skill_activation', onHistoryChanged);
279
+ session.off('memory_event', onHistoryChanged);
280
+ session.off('turn_source', onTurnSource);
281
+ session.off('status_changed', onStatusChanged);
282
+ openPrompts?.release();
283
+ };
284
+ }
285
+
286
+ /** The turn's result without the session's history: that only grows, and `get-history` pages it. */
287
+ function withoutHistory(result: IExecutionResult): TWireExecutionResult {
288
+ const { history: _history, ...rest } = result;
289
+ return rest;
290
+ }