agl 22.0.4 → 24.0.0

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 (44) hide show
  1. package/changelog.md +21 -0
  2. package/dist_serve/bundle.js +2418 -2408
  3. package/dist_serve/bundle.js.map +1 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/classes.authstore.js +2 -2
  6. package/dist_ts/classes.browserresourcehost.d.ts +1 -0
  7. package/dist_ts/classes.browserresourcehost.js +7 -1
  8. package/dist_ts/classes.controller.d.ts +37 -5
  9. package/dist_ts/classes.controller.js +1491 -249
  10. package/dist_ts/classes.flexservice.d.ts +5 -0
  11. package/dist_ts/classes.flexservice.js +131 -4
  12. package/dist_ts/classes.opencodeclient.d.ts +2 -0
  13. package/dist_ts/classes.opencodeclient.js +23 -1
  14. package/dist_ts/classes.validation.d.ts +9 -1
  15. package/dist_ts/classes.validation.js +62 -7
  16. package/dist_ts/interfaces.flexipc.d.ts +18 -1
  17. package/dist_ts/interfaces.flexipc.js +42 -8
  18. package/dist_ts_interfaces/browsertransport.d.ts +72 -0
  19. package/dist_ts_interfaces/browsertransport.js +47 -0
  20. package/dist_ts_interfaces/index.d.ts +1 -0
  21. package/dist_ts_interfaces/index.js +2 -1
  22. package/dist_ts_interfaces/interfaces.d.ts +42 -7
  23. package/dist_ts_interfaces/interfaces.js +5 -3
  24. package/dist_ts_interfaces/requests.d.ts +20 -21
  25. package/package.json +7 -7
  26. package/readme.md +12 -2
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/classes.authstore.ts +1 -1
  29. package/ts/classes.browserresourcehost.ts +7 -0
  30. package/ts/classes.controller.ts +1960 -305
  31. package/ts/classes.flexservice.ts +132 -2
  32. package/ts/classes.opencodeclient.ts +19 -0
  33. package/ts/classes.validation.ts +77 -6
  34. package/ts/interfaces.flexipc.ts +65 -7
  35. package/ts_interfaces/browsertransport.ts +126 -0
  36. package/ts_interfaces/index.ts +1 -0
  37. package/ts_interfaces/interfaces.ts +46 -11
  38. package/ts_interfaces/requests.ts +23 -30
  39. package/ts_web/00_commitinfo_data.ts +1 -1
  40. package/ts_web/classes.browserviewtransport.ts +532 -0
  41. package/ts_web/classes.controllersocketclient.ts +119 -29
  42. package/ts_web/elements.harnesscontrollerapp.ts +1727 -287
  43. package/ts_web/index.ts +1 -0
  44. package/ts_web/plugins.ts +2 -0
@@ -64,7 +64,9 @@ import {
64
64
  flexIpcMaximumPromptQueueEntries,
65
65
  flexIpcMaximumQueuedSessionChangeBytes,
66
66
  flexIpcMaximumQueuedSessionChanges,
67
+ flexIpcMaximumDeltaBytes,
67
68
  flexIpcProtocolVersion,
69
+ flexIpcTargetDeltaBytes,
68
70
  isFlexHostResponseResult,
69
71
  } from './interfaces.flexipc.js';
70
72
  import { openCodeOAuthAuthFromProviderCredential } from './functions.opencodeauth.js';
@@ -78,6 +80,24 @@ type TFlexHarnessEvent = plugins.flexharness.TFlexHarnessEvent;
78
80
 
79
81
  const flexFailureStatusTimeoutMs = 1_000;
80
82
 
83
+ const fragmentUtf8 = (valueArg: string, targetBytesArg: number): string[] => {
84
+ const fragments: string[] = [];
85
+ let fragment = '';
86
+ let fragmentBytes = 0;
87
+ for (const codePoint of valueArg) {
88
+ const codePointBytes = Buffer.byteLength(codePoint, 'utf8');
89
+ if (fragment && fragmentBytes + codePointBytes > targetBytesArg) {
90
+ fragments.push(fragment);
91
+ fragment = '';
92
+ fragmentBytes = 0;
93
+ }
94
+ fragment += codePoint;
95
+ fragmentBytes += codePointBytes;
96
+ }
97
+ if (fragment) fragments.push(fragment);
98
+ return fragments;
99
+ };
100
+
81
101
  const waitForFlexServiceDeadline = async <TValue>(
82
102
  promiseArg: Promise<TValue>,
83
103
  timeoutMsArg: number,
@@ -452,7 +472,9 @@ export class FlexService {
452
472
  private browserFrameReservationCount = 0;
453
473
  private browserFrameReservationBytes = 0;
454
474
  private readonly sessionChangeQueue = new Map<string, TFlexChildEvent>();
475
+ private sessionDeltaTailKeys = new Map<string, string>();
455
476
  private sessionChangeQueueBytes = 0;
477
+ private sessionChangeQueueTailKey: string | undefined;
456
478
  private sessionChangeFlushRunning = false;
457
479
  private hostRequestAdmissionOpen = true;
458
480
  private admissionOpen = true;
@@ -2517,9 +2539,28 @@ export class FlexService {
2517
2539
  part: eventArg.part,
2518
2540
  });
2519
2541
  }
2542
+ if (eventArg.type === 'part.delta') {
2543
+ this.queuePartDelta({
2544
+ type: 'part.delta',
2545
+ scopeId: eventArg.scopeId,
2546
+ sessionId: eventArg.sessionId,
2547
+ runId: eventArg.runId,
2548
+ messageId: eventArg.messageId,
2549
+ messageIndex: eventArg.messageIndex,
2550
+ partId: eventArg.partId,
2551
+ partIndex: eventArg.partIndex,
2552
+ sequence: eventArg.sequence,
2553
+ timestamp: eventArg.timestamp,
2554
+ partType: eventArg.partType,
2555
+ delta: eventArg.delta,
2556
+ baseTextUtf8Bytes: eventArg.baseTextUtf8Bytes,
2557
+ textUtf8Bytes: eventArg.textUtf8Bytes,
2558
+ });
2559
+ this.queueSessionChange(eventArg.scopeId, eventArg.sessionId);
2560
+ return;
2561
+ }
2520
2562
  if (
2521
2563
  (eventArg.type === 'part.started'
2522
- || eventArg.type === 'part.delta'
2523
2564
  || eventArg.type === 'part.completed')
2524
2565
  && eventArg.part.type === 'reasoning'
2525
2566
  ) {
@@ -2538,7 +2579,6 @@ export class FlexService {
2538
2579
  }
2539
2580
  if (
2540
2581
  (eventArg.type === 'part.started'
2541
- || eventArg.type === 'part.delta'
2542
2582
  || eventArg.type === 'part.completed')
2543
2583
  && eventArg.part.type === 'text'
2544
2584
  ) {
@@ -2949,10 +2989,23 @@ export class FlexService {
2949
2989
  }), 'utf8');
2950
2990
  }
2951
2991
 
2992
+ private getSessionDeltaTailKeys(): Map<string, string> {
2993
+ this.sessionDeltaTailKeys ??= new Map<string, string>();
2994
+ return this.sessionDeltaTailKeys;
2995
+ }
2996
+
2952
2997
  private deleteSessionChangeQueueEntry(keyArg: string): void {
2953
2998
  const existing = this.sessionChangeQueue.get(keyArg);
2954
2999
  if (!existing) return;
2955
3000
  this.sessionChangeQueue.delete(keyArg);
3001
+ const deltaTailKeys = this.getSessionDeltaTailKeys();
3002
+ for (const [partKey, tailKey] of deltaTailKeys) {
3003
+ if (tailKey === keyArg) deltaTailKeys.delete(partKey);
3004
+ }
3005
+ if (this.sessionChangeQueueTailKey === keyArg) {
3006
+ this.sessionChangeQueueTailKey = undefined;
3007
+ for (const key of this.sessionChangeQueue.keys()) this.sessionChangeQueueTailKey = key;
3008
+ }
2956
3009
  this.sessionChangeQueueBytes = Math.max(
2957
3010
  0,
2958
3011
  (this.sessionChangeQueueBytes ?? 0) - this.sessionChangeEventBytes(existing),
@@ -2961,7 +3014,9 @@ export class FlexService {
2961
3014
 
2962
3015
  private clearSessionChangeQueue(): void {
2963
3016
  this.sessionChangeQueue.clear();
3017
+ this.getSessionDeltaTailKeys().clear();
2964
3018
  this.sessionChangeQueueBytes = 0;
3019
+ this.sessionChangeQueueTailKey = undefined;
2965
3020
  }
2966
3021
 
2967
3022
  private setSessionChangeQueueEntry(keyArg: string, eventArg: TFlexChildEvent): boolean {
@@ -2978,6 +3033,7 @@ export class FlexService {
2978
3033
  || nextBytes > flexIpcMaximumQueuedSessionChangeBytes
2979
3034
  ) return false;
2980
3035
  this.sessionChangeQueue.set(keyArg, eventArg);
3036
+ if (!existing) this.sessionChangeQueueTailKey = keyArg;
2981
3037
  this.sessionChangeQueueBytes = nextBytes;
2982
3038
  return true;
2983
3039
  }
@@ -2987,6 +3043,7 @@ export class FlexService {
2987
3043
  this.clearSessionChangeQueue();
2988
3044
  this.sessionChangeQueue.set('*', event);
2989
3045
  this.sessionChangeQueueBytes = this.sessionChangeEventBytes(event);
3046
+ this.sessionChangeQueueTailKey = '*';
2990
3047
  }
2991
3048
 
2992
3049
  private queueToolChange(eventArg: Extract<TFlexChildEvent, {
@@ -3026,6 +3083,78 @@ export class FlexService {
3026
3083
  this.startSessionChangeFlush();
3027
3084
  }
3028
3085
 
3086
+ private queuePartDelta(eventArg: Extract<TFlexChildEvent, { type: 'part.delta' }>): void {
3087
+ if (this.disposing || this.disposed || this.sessionChangeQueue.has('*')) return;
3088
+ const encodedDeltaBytes = Buffer.byteLength(eventArg.delta, 'utf8');
3089
+ const boundaryCorrection = eventArg.textUtf8Bytes
3090
+ - eventArg.baseTextUtf8Bytes
3091
+ - encodedDeltaBytes;
3092
+ const fragments = fragmentUtf8(eventArg.delta, flexIpcTargetDeltaBytes);
3093
+ if (fragments.length === 0) {
3094
+ this.collapseSessionChangeQueue();
3095
+ this.startSessionChangeFlush();
3096
+ return;
3097
+ }
3098
+ let baseTextUtf8Bytes = eventArg.baseTextUtf8Bytes;
3099
+ for (const [fragmentIndex, fragment] of fragments.entries()) {
3100
+ const textUtf8Bytes = baseTextUtf8Bytes
3101
+ + Buffer.byteLength(fragment, 'utf8')
3102
+ + (fragmentIndex === 0 ? boundaryCorrection : 0);
3103
+ if (textUtf8Bytes <= baseTextUtf8Bytes) {
3104
+ this.collapseSessionChangeQueue();
3105
+ this.startSessionChangeFlush();
3106
+ return;
3107
+ }
3108
+ const fragmentEvent: Extract<TFlexChildEvent, { type: 'part.delta' }> = {
3109
+ ...eventArg,
3110
+ delta: fragment,
3111
+ baseTextUtf8Bytes,
3112
+ textUtf8Bytes,
3113
+ };
3114
+ if (!this.enqueuePartDeltaFragment(fragmentEvent)) {
3115
+ this.collapseSessionChangeQueue();
3116
+ this.startSessionChangeFlush();
3117
+ return;
3118
+ }
3119
+ baseTextUtf8Bytes = textUtf8Bytes;
3120
+ }
3121
+ if (baseTextUtf8Bytes !== eventArg.textUtf8Bytes) this.collapseSessionChangeQueue();
3122
+ this.startSessionChangeFlush();
3123
+ }
3124
+
3125
+ private enqueuePartDeltaFragment(
3126
+ eventArg: Extract<TFlexChildEvent, { type: 'part.delta' }>,
3127
+ ): boolean {
3128
+ const partKey = `${eventArg.scopeId}\0${eventArg.sessionId}\0${eventArg.partType}\0${eventArg.partId}`;
3129
+ const deltaTailKeys = this.getSessionDeltaTailKeys();
3130
+ const tailKey = deltaTailKeys.get(partKey);
3131
+ const tail = tailKey ? this.sessionChangeQueue.get(tailKey) : undefined;
3132
+ if (
3133
+ tail?.type === 'part.delta'
3134
+ && tailKey === this.sessionChangeQueueTailKey
3135
+ && tail.textUtf8Bytes === eventArg.baseTextUtf8Bytes
3136
+ && tail.runId === eventArg.runId
3137
+ && tail.messageId === eventArg.messageId
3138
+ && tail.messageIndex === eventArg.messageIndex
3139
+ && tail.partIndex === eventArg.partIndex
3140
+ && Buffer.byteLength(tail.delta, 'utf8') + Buffer.byteLength(eventArg.delta, 'utf8')
3141
+ <= flexIpcMaximumDeltaBytes
3142
+ ) {
3143
+ const merged: Extract<TFlexChildEvent, { type: 'part.delta' }> = {
3144
+ ...tail,
3145
+ sequence: eventArg.sequence,
3146
+ timestamp: eventArg.timestamp,
3147
+ delta: `${tail.delta}${eventArg.delta}`,
3148
+ textUtf8Bytes: eventArg.textUtf8Bytes,
3149
+ };
3150
+ return this.setSessionChangeQueueEntry(tailKey!, merged);
3151
+ }
3152
+ const key = `${partKey}\0${eventArg.baseTextUtf8Bytes}\0${eventArg.sequence}`;
3153
+ if (!this.setSessionChangeQueueEntry(key, eventArg)) return false;
3154
+ deltaTailKeys.set(partKey, key);
3155
+ return true;
3156
+ }
3157
+
3029
3158
 
3030
3159
  private queueSessionChange(scopeIdArg: string, sessionIdArg: string): void {
3031
3160
  const globalKey = '*';
@@ -3069,6 +3198,7 @@ export class FlexService {
3069
3198
  }
3070
3199
  } catch {
3071
3200
  this.clearSessionChangeQueue();
3201
+ await this.closeTransport().catch(() => undefined);
3072
3202
  } finally {
3073
3203
  this.sessionChangeFlushRunning = false;
3074
3204
  if (this.sessionChangeQueue.size > 0 && !this.disposing && !this.disposed) {
@@ -54,6 +54,7 @@ const maxLiveToolExecutionBytes = controllerMaxToolEventBytes - (8 * 1024);
54
54
  const maxLiveMessageSnapshotBytes = controllerMaxLiveMessageEventBytes - (8 * 1024);
55
55
  const maxLiveMessagePartCacheEntries = 512;
56
56
  const maxLiveMessagePartCacheBytes = 4 * 1024 * 1024;
57
+ const maxLiveMessageEventIds = 4_096;
57
58
  const maxMetadataStringBytes = 16 * 1024;
58
59
  const maxMetadataNodes = 4096;
59
60
  const maxCollectionEntries = 512;
@@ -1816,6 +1817,7 @@ export class OpenCodeClientAdapter {
1816
1817
  private readonly sessionMetricsCache = new Map<string, ISessionMetricsCacheEntry>();
1817
1818
  private readonly liveMessagePartCache = new Map<string, IOpenCodeLiveMessagePartCacheEntry>();
1818
1819
  private liveMessagePartCacheBytes = 0;
1820
+ private readonly liveMessageEventIds = new Set<string>();
1819
1821
 
1820
1822
  constructor(options: IOpenCodeClientOptions) {
1821
1823
  this.directory = plugins.fs.realpathSync.native(options.directory);
@@ -3651,6 +3653,21 @@ export class OpenCodeClientAdapter {
3651
3653
  private clearLiveMessagePartCache(): void {
3652
3654
  this.liveMessagePartCache.clear();
3653
3655
  this.liveMessagePartCacheBytes = 0;
3656
+ this.liveMessageEventIds.clear();
3657
+ }
3658
+
3659
+ private acceptLiveMessageEventId(payloadArg: Record<string, unknown>): boolean {
3660
+ if (typeof payloadArg.id !== 'string' || Buffer.byteLength(payloadArg.id, 'utf8') > 512) {
3661
+ return true;
3662
+ }
3663
+ if (this.liveMessageEventIds.has(payloadArg.id)) return false;
3664
+ this.liveMessageEventIds.add(payloadArg.id);
3665
+ while (this.liveMessageEventIds.size > maxLiveMessageEventIds) {
3666
+ const oldest = this.liveMessageEventIds.values().next().value;
3667
+ if (oldest === undefined) break;
3668
+ this.liveMessageEventIds.delete(oldest);
3669
+ }
3670
+ return true;
3654
3671
  }
3655
3672
 
3656
3673
  private clearLiveMessagePartsForSession(directoryArg: string, sessionIdArg: string): void {
@@ -3743,6 +3760,7 @@ export class OpenCodeClientAdapter {
3743
3760
  };
3744
3761
 
3745
3762
  if (payload.type === 'message.part.updated') {
3763
+ if (!this.acceptLiveMessageEventId(payload)) return undefined;
3746
3764
  if (!isRecord(properties.part)) {
3747
3765
  this.clearLiveMessagePartCache();
3748
3766
  return {
@@ -3819,6 +3837,7 @@ export class OpenCodeClientAdapter {
3819
3837
  }
3820
3838
 
3821
3839
  if (payload.type === 'message.part.delta') {
3840
+ if (!this.acceptLiveMessageEventId(payload)) return undefined;
3822
3841
  try {
3823
3842
  const validatedSessionId = requireString(
3824
3843
  properties.sessionID,
@@ -5,6 +5,7 @@ import {
5
5
  controllerMaxDraftAttachmentTotalBytes,
6
6
  controllerMaxDraftAttachments,
7
7
  controllerMaxDraftTextBytes,
8
+ controllerBrowserTransportProtocol,
8
9
  controllerRuntimeIdKey,
9
10
  controllerSessionHarnessIds,
10
11
  } from '../ts_interfaces/index.js';
@@ -1065,7 +1066,7 @@ const assertBrowserViewport = (
1065
1066
  || typeof value.deviceScaleFactor !== 'number'
1066
1067
  || !Number.isFinite(value.deviceScaleFactor)
1067
1068
  || value.deviceScaleFactor < 0.25
1068
- || value.deviceScaleFactor > 4
1069
+ || value.deviceScaleFactor > 3
1069
1070
  ) throw new Error(`${nameArg} is invalid.`);
1070
1071
  return value as unknown as interfaces.IControllerBrowserViewport;
1071
1072
  };
@@ -1121,21 +1122,50 @@ export const assertBrowserViewCloseRequest = (
1121
1122
  valueArg: unknown,
1122
1123
  ): interfaces.IReq_ControllerBrowserViewClose['request'] => {
1123
1124
  const value = assertObject(valueArg, 'browser view close request');
1124
- assertKeys(value, ['viewId']);
1125
- return { viewId: assertIdentifier(value.viewId, 'viewId') };
1125
+ assertKeys(value, ['viewId', 'expectedStreamGeneration']);
1126
+ return {
1127
+ viewId: assertIdentifier(value.viewId, 'viewId'),
1128
+ expectedStreamGeneration: assertBrowserStreamGeneration(value.expectedStreamGeneration),
1129
+ };
1126
1130
  };
1127
1131
 
1128
1132
  export const assertBrowserViewActivateRequest = (
1129
1133
  valueArg: unknown,
1130
1134
  ): interfaces.IReq_ControllerBrowserViewActivate['request'] => {
1131
1135
  const value = assertObject(valueArg, 'browser view activation request');
1132
- assertKeys(value, ['viewId']);
1133
- return { viewId: assertIdentifier(value.viewId, 'viewId') };
1136
+ assertKeys(value, ['viewId', 'streamGeneration']);
1137
+ return {
1138
+ viewId: assertIdentifier(value.viewId, 'viewId'),
1139
+ streamGeneration: assertBrowserStreamGeneration(value.streamGeneration),
1140
+ };
1141
+ };
1142
+
1143
+ const assertBrowserStreamGeneration = (valueArg: unknown): number => {
1144
+ if (!Number.isSafeInteger(valueArg) || (valueArg as number) < 1) {
1145
+ throw new Error('streamGeneration is invalid.');
1146
+ }
1147
+ return valueArg as number;
1134
1148
  };
1135
1149
 
1150
+ export const assertBrowserViewRecoverRequest = (
1151
+ valueArg: unknown,
1152
+ ): interfaces.IReq_ControllerBrowserViewRecover['request'] => {
1153
+ const value = assertObject(valueArg, 'browser view recovery request');
1154
+ assertKeys(value, ['viewId', 'expectedStreamGeneration']);
1155
+ return {
1156
+ viewId: assertIdentifier(value.viewId, 'viewId'),
1157
+ expectedStreamGeneration: assertBrowserStreamGeneration(value.expectedStreamGeneration),
1158
+ };
1159
+ };
1160
+
1161
+ interface IBrowserViewOperationEnvelope {
1162
+ viewId: string;
1163
+ operation: interfaces.TControllerBrowserViewOperation;
1164
+ }
1165
+
1136
1166
  export const assertBrowserViewOperateRequest = (
1137
1167
  valueArg: unknown,
1138
- ): interfaces.IReq_ControllerBrowserViewOperate['request'] => {
1168
+ ): IBrowserViewOperationEnvelope => {
1139
1169
  const value = assertObject(valueArg, 'browser view operation request');
1140
1170
  assertKeys(value, ['viewId', 'operation']);
1141
1171
  const operation = assertObject(value.operation, 'operation');
@@ -1304,6 +1334,47 @@ export const assertBrowserViewOperateRequest = (
1304
1334
  throw new Error('operation.type is unsupported.');
1305
1335
  };
1306
1336
 
1337
+ export const assertBrowserViewOperation = (
1338
+ valueArg: unknown,
1339
+ ): interfaces.TControllerBrowserViewOperation => assertBrowserViewOperateRequest({
1340
+ viewId: 'browser-operation',
1341
+ operation: valueArg,
1342
+ }).operation;
1343
+
1344
+ export const assertControllerBrowserClientPacket = (
1345
+ valueArg: unknown,
1346
+ ): interfaces.TControllerBrowserClientPacket => {
1347
+ const value = assertObject(valueArg, 'browser transport packet');
1348
+ if (value.protocol !== controllerBrowserTransportProtocol) {
1349
+ throw new Error('browser transport protocol is invalid.');
1350
+ }
1351
+ const streamGeneration = assertBrowserStreamGeneration(value.streamGeneration);
1352
+ if (!Number.isSafeInteger(value.operationId) || (value.operationId as number) < 1) {
1353
+ throw new Error('operationId is invalid.');
1354
+ }
1355
+ const operationId = value.operationId as number;
1356
+ if (value.type === 'operation') {
1357
+ assertKeys(value, ['protocol', 'streamGeneration', 'type', 'operationId', 'operation']);
1358
+ return {
1359
+ protocol: controllerBrowserTransportProtocol,
1360
+ streamGeneration,
1361
+ type: 'operation',
1362
+ operationId,
1363
+ operation: assertBrowserViewOperation(value.operation),
1364
+ };
1365
+ }
1366
+ if (value.type === 'cancel') {
1367
+ assertKeys(value, ['protocol', 'streamGeneration', 'type', 'operationId']);
1368
+ return {
1369
+ protocol: controllerBrowserTransportProtocol,
1370
+ streamGeneration,
1371
+ type: 'cancel',
1372
+ operationId,
1373
+ };
1374
+ }
1375
+ throw new Error('browser transport packet type is unsupported.');
1376
+ };
1377
+
1307
1378
  export const assertTerminalCreateRequest = (
1308
1379
  valueArg: unknown,
1309
1380
  ): interfaces.IReq_ControllerTerminalCreate['request'] => {
@@ -32,8 +32,10 @@ export type TFlexTurnReversionCaptureInspectionV2 =
32
32
  export type TFlexTurnReversionApplyInspection =
33
33
  plugins.flexharness.IFlexTurnReversionApplyInspection;
34
34
 
35
- export const flexIpcProtocolVersion = 14 as const;
35
+ export const flexIpcProtocolVersion = 15 as const;
36
36
  export const flexIpcMaximumFrameBytes = 2 * 1024 * 1024;
37
+ export const flexIpcTargetDeltaBytes = 16 * 1024;
38
+ export const flexIpcMaximumDeltaBytes = 32 * 1024;
37
39
  export const flexIpcMaximumPendingRequests = 64;
38
40
  export const flexIpcMaximumBrowserChannels = 128;
39
41
  export const flexIpcMaximumBrowserFrameBytes = 256 * 1024 + 4;
@@ -764,6 +766,22 @@ export type TFlexChildEvent =
764
766
  status: 'running' | 'completed';
765
767
  part: TFlexTextMessagePart;
766
768
  }
769
+ | {
770
+ type: 'part.delta';
771
+ scopeId: string;
772
+ sessionId: string;
773
+ runId: string;
774
+ messageId: string;
775
+ messageIndex: number;
776
+ partId: string;
777
+ partIndex: number;
778
+ sequence: number;
779
+ timestamp: string;
780
+ partType: 'text' | 'reasoning';
781
+ delta: string;
782
+ baseTextUtf8Bytes: number;
783
+ textUtf8Bytes: number;
784
+ }
767
785
  | {
768
786
  type: 'prompt.queued';
769
787
  scopeId: string;
@@ -945,6 +963,20 @@ const isBoundedString = (
945
963
  && (allowEmptyArg || valueArg.length > 0)
946
964
  && Buffer.byteLength(valueArg, 'utf8') <= maximumBytesArg;
947
965
 
966
+ const hasExactDeltaUtf8Coordinates = (
967
+ deltaArg: string,
968
+ baseTextUtf8BytesArg: number,
969
+ textUtf8BytesArg: number,
970
+ ): boolean => {
971
+ const encodedDeltaBytes = Buffer.byteLength(deltaArg, 'utf8');
972
+ const increment = textUtf8BytesArg - baseTextUtf8BytesArg;
973
+ if (increment === encodedDeltaBytes) return true;
974
+ const firstCodeUnit = deltaArg.charCodeAt(0);
975
+ return firstCodeUnit >= 0xDC00
976
+ && firstCodeUnit <= 0xDFFF
977
+ && increment === encodedDeltaBytes - 2;
978
+ };
979
+
948
980
  const isIdentifier = (valueArg: unknown): valueArg is string =>
949
981
  isBoundedString(valueArg, 512) && identifierPattern.test(valueArg);
950
982
 
@@ -1244,15 +1276,15 @@ const isUsage = (valueArg: unknown): boolean => isFlexPlainObject(valueArg)
1244
1276
  ])
1245
1277
  && Object.values(valueArg).every(isNonNegativeInteger);
1246
1278
 
1247
- const isMessagePart = (valueArg: unknown): boolean => {
1279
+ const isMessagePart = (valueArg: unknown, textByteLimitArg = 96 * 1024): boolean => {
1248
1280
  if (!isFlexPlainObject(valueArg) || !isIdentifier(valueArg.partId)) return false;
1249
1281
  switch (valueArg.type) {
1250
1282
  case 'text':
1251
1283
  return hasFlexExactKeys(valueArg, ['partId', 'type', 'text'])
1252
- && isBoundedString(valueArg.text, 96 * 1024, true);
1284
+ && isBoundedString(valueArg.text, textByteLimitArg, true);
1253
1285
  case 'reasoning':
1254
1286
  return hasFlexExactKeys(valueArg, ['partId', 'type', 'text', 'status'])
1255
- && isBoundedString(valueArg.text, 96 * 1024, true)
1287
+ && isBoundedString(valueArg.text, textByteLimitArg, true)
1256
1288
  && ['running', 'completed', 'cancelled'].includes(String(valueArg.status));
1257
1289
  case 'tool':
1258
1290
  return hasFlexExactKeys(
@@ -1302,7 +1334,7 @@ export const isFlexPublicMessage = (valueArg: unknown): valueArg is TFlexMessage
1302
1334
  && (valueArg.completedAt === undefined || isIsoDate(valueArg.completedAt))
1303
1335
  && Array.isArray(valueArg.parts)
1304
1336
  && valueArg.parts.length <= 2000
1305
- && valueArg.parts.every(isMessagePart)
1337
+ && valueArg.parts.every((partArg) => isMessagePart(partArg))
1306
1338
  && (valueArg.model === undefined || isModelIdentity(valueArg.model))
1307
1339
  && (valueArg.usage === undefined || isUsage(valueArg.usage))
1308
1340
  && (valueArg.error === undefined || isBoundedString(valueArg.error, 2048));
@@ -2339,7 +2371,7 @@ const isFlexChildEvent = (valueArg: unknown): valueArg is TFlexChildEvent => {
2339
2371
  && isIsoDate(valueArg.timestamp)
2340
2372
  && isFlexPlainObject(valueArg.part)
2341
2373
  && valueArg.part.type === 'reasoning'
2342
- && isMessagePart(valueArg.part);
2374
+ && isMessagePart(valueArg.part, flexIpcMaximumFrameBytes - (16 * 1024));
2343
2375
  case 'text.updated':
2344
2376
  return hasFlexExactKeys(valueArg, [
2345
2377
  'type', 'scopeId', 'sessionId', 'runId', 'messageId',
@@ -2357,7 +2389,33 @@ const isFlexChildEvent = (valueArg: unknown): valueArg is TFlexChildEvent => {
2357
2389
  && ['running', 'completed'].includes(String(valueArg.status))
2358
2390
  && isFlexPlainObject(valueArg.part)
2359
2391
  && valueArg.part.type === 'text'
2360
- && isMessagePart(valueArg.part);
2392
+ && isMessagePart(valueArg.part, flexIpcMaximumFrameBytes - (16 * 1024));
2393
+ case 'part.delta':
2394
+ return hasFlexExactKeys(valueArg, [
2395
+ 'type', 'scopeId', 'sessionId', 'runId', 'messageId', 'messageIndex', 'partId',
2396
+ 'partIndex', 'sequence', 'timestamp', 'partType', 'delta',
2397
+ 'baseTextUtf8Bytes', 'textUtf8Bytes',
2398
+ ])
2399
+ && isIdentifier(valueArg.scopeId)
2400
+ && isIdentifier(valueArg.sessionId)
2401
+ && isIdentifier(valueArg.runId)
2402
+ && isIdentifier(valueArg.messageId)
2403
+ && isIdentifier(valueArg.partId)
2404
+ && isNonNegativeInteger(valueArg.messageIndex)
2405
+ && isNonNegativeInteger(valueArg.partIndex)
2406
+ && isNonNegativeInteger(valueArg.sequence)
2407
+ && valueArg.sequence >= 1
2408
+ && isIsoDate(valueArg.timestamp)
2409
+ && (valueArg.partType === 'text' || valueArg.partType === 'reasoning')
2410
+ && isBoundedString(valueArg.delta, flexIpcMaximumDeltaBytes)
2411
+ && isNonNegativeInteger(valueArg.baseTextUtf8Bytes)
2412
+ && isNonNegativeInteger(valueArg.textUtf8Bytes)
2413
+ && valueArg.textUtf8Bytes > valueArg.baseTextUtf8Bytes
2414
+ && hasExactDeltaUtf8Coordinates(
2415
+ valueArg.delta,
2416
+ valueArg.baseTextUtf8Bytes,
2417
+ valueArg.textUtf8Bytes,
2418
+ );
2361
2419
  case 'prompt.queued':
2362
2420
  case 'prompt.started':
2363
2421
  case 'prompt.running':
@@ -0,0 +1,126 @@
1
+ import type {
2
+ IControllerBrowserFrameMetadata,
3
+ IControllerBrowserViewState,
4
+ TControllerBrowserViewOperation,
5
+ } from './interfaces.js';
6
+
7
+ export const controllerBrowserTransportProtocol = 'agl-browser-v24' as const;
8
+ export const controllerBrowserTransportEventContentType =
9
+ 'application/vnd.agl.browser-events.v24' as const;
10
+ export const controllerBrowserTransportOperationContentType =
11
+ 'application/vnd.agl.browser-operations.v24' as const;
12
+ export const controllerBrowserTransportMaximumJsonBytes = 64 * 1024;
13
+ export const controllerBrowserTransportMaximumFrameBytes = 4 * 1024 * 1024;
14
+ export const controllerBrowserTransportPairOpenTimeoutMs = 9_000;
15
+ export const controllerBrowserTransportRecoveryGraceMs = 15_000;
16
+ export const controllerBrowserTransportMaximumRecoveryAttempts = 3;
17
+ export const controllerBrowserTransportMaximumPendingOperations = 64;
18
+ export const controllerBrowserTransportMaximumQueuedOperationBytes = 256 * 1024;
19
+ export const controllerBrowserTransportMaximumActiveAcknowledgements = 16;
20
+ export const controllerBrowserTransportMaximumPendingEvents = 16;
21
+ export const controllerBrowserTransportMaximumPendingEventBytes = 16 * 1024 * 1024;
22
+
23
+ interface IControllerBrowserTransportPacketBase {
24
+ protocol: typeof controllerBrowserTransportProtocol;
25
+ streamGeneration: number;
26
+ }
27
+
28
+ export interface IControllerBrowserOperationPacket extends IControllerBrowserTransportPacketBase {
29
+ type: 'operation';
30
+ operationId: number;
31
+ operation: TControllerBrowserViewOperation;
32
+ }
33
+
34
+ export interface IControllerBrowserCancelPacket extends IControllerBrowserTransportPacketBase {
35
+ type: 'cancel';
36
+ operationId: number;
37
+ }
38
+
39
+ export type TControllerBrowserClientPacket =
40
+ | IControllerBrowserOperationPacket
41
+ | IControllerBrowserCancelPacket;
42
+
43
+ export type TControllerBrowserOperationResult =
44
+ | {
45
+ status: 'succeeded';
46
+ accepted?: boolean;
47
+ state?: IControllerBrowserViewState;
48
+ }
49
+ | { status: 'failed'; error: { code: string; message: string } }
50
+ | { status: 'cancelled' }
51
+ | { status: 'superseded' };
52
+
53
+ export interface IControllerBrowserStatePacket extends IControllerBrowserTransportPacketBase {
54
+ type: 'state';
55
+ state: IControllerBrowserViewState;
56
+ }
57
+
58
+ export interface IControllerBrowserErrorPacket extends IControllerBrowserTransportPacketBase {
59
+ type: 'error';
60
+ error: { code: string; message: string; fatal: boolean; tabId?: string };
61
+ }
62
+
63
+ export interface IControllerBrowserOperationResultPacket
64
+ extends IControllerBrowserTransportPacketBase {
65
+ type: 'operationResult';
66
+ operationId: number;
67
+ result: TControllerBrowserOperationResult;
68
+ }
69
+
70
+ export interface IControllerBrowserFramePacket extends IControllerBrowserTransportPacketBase {
71
+ type: 'frame';
72
+ frame: IControllerBrowserFrameMetadata;
73
+ }
74
+
75
+ export type TControllerBrowserServerPacket =
76
+ | IControllerBrowserStatePacket
77
+ | IControllerBrowserErrorPacket
78
+ | IControllerBrowserOperationResultPacket
79
+ | IControllerBrowserFramePacket;
80
+
81
+ export class ControllerBrowserTransportProtocolError extends Error {
82
+ constructor(messageArg: string, optionsArg?: ErrorOptions) {
83
+ super(messageArg, optionsArg);
84
+ this.name = 'ControllerBrowserTransportProtocolError';
85
+ }
86
+ }
87
+
88
+ const textEncoder = new TextEncoder();
89
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
90
+
91
+ export const encodeControllerBrowserTransportPacket = (
92
+ packetArg: TControllerBrowserClientPacket | TControllerBrowserServerPacket,
93
+ ): Uint8Array => {
94
+ let serialized: string;
95
+ try {
96
+ serialized = JSON.stringify(packetArg);
97
+ } catch (errorArg) {
98
+ throw new ControllerBrowserTransportProtocolError(
99
+ 'The browser transport packet is not JSON serializable.',
100
+ { cause: errorArg },
101
+ );
102
+ }
103
+ const bytes = textEncoder.encode(serialized);
104
+ if (bytes.byteLength > controllerBrowserTransportMaximumJsonBytes) {
105
+ throw new ControllerBrowserTransportProtocolError(
106
+ 'The browser transport packet exceeds the JSON byte limit.',
107
+ );
108
+ }
109
+ return bytes;
110
+ };
111
+
112
+ export const decodeControllerBrowserTransportPacket = (bytesArg: Uint8Array): unknown => {
113
+ if (bytesArg.byteLength > controllerBrowserTransportMaximumJsonBytes) {
114
+ throw new ControllerBrowserTransportProtocolError(
115
+ 'The browser transport packet exceeds the JSON byte limit.',
116
+ );
117
+ }
118
+ try {
119
+ return JSON.parse(textDecoder.decode(bytesArg));
120
+ } catch (errorArg) {
121
+ throw new ControllerBrowserTransportProtocolError(
122
+ 'The browser transport packet is not valid UTF-8 JSON.',
123
+ { cause: errorArg },
124
+ );
125
+ }
126
+ };
@@ -1,2 +1,3 @@
1
1
  export * from './interfaces.js';
2
+ export * from './browsertransport.js';
2
3
  export * from './requests.js';