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.
- package/changelog.md +21 -0
- package/dist_serve/bundle.js +2418 -2408
- package/dist_serve/bundle.js.map +1 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.authstore.js +2 -2
- package/dist_ts/classes.browserresourcehost.d.ts +1 -0
- package/dist_ts/classes.browserresourcehost.js +7 -1
- package/dist_ts/classes.controller.d.ts +37 -5
- package/dist_ts/classes.controller.js +1491 -249
- package/dist_ts/classes.flexservice.d.ts +5 -0
- package/dist_ts/classes.flexservice.js +131 -4
- package/dist_ts/classes.opencodeclient.d.ts +2 -0
- package/dist_ts/classes.opencodeclient.js +23 -1
- package/dist_ts/classes.validation.d.ts +9 -1
- package/dist_ts/classes.validation.js +62 -7
- package/dist_ts/interfaces.flexipc.d.ts +18 -1
- package/dist_ts/interfaces.flexipc.js +42 -8
- package/dist_ts_interfaces/browsertransport.d.ts +72 -0
- package/dist_ts_interfaces/browsertransport.js +47 -0
- package/dist_ts_interfaces/index.d.ts +1 -0
- package/dist_ts_interfaces/index.js +2 -1
- package/dist_ts_interfaces/interfaces.d.ts +42 -7
- package/dist_ts_interfaces/interfaces.js +5 -3
- package/dist_ts_interfaces/requests.d.ts +20 -21
- package/package.json +7 -7
- package/readme.md +12 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.authstore.ts +1 -1
- package/ts/classes.browserresourcehost.ts +7 -0
- package/ts/classes.controller.ts +1960 -305
- package/ts/classes.flexservice.ts +132 -2
- package/ts/classes.opencodeclient.ts +19 -0
- package/ts/classes.validation.ts +77 -6
- package/ts/interfaces.flexipc.ts +65 -7
- package/ts_interfaces/browsertransport.ts +126 -0
- package/ts_interfaces/index.ts +1 -0
- package/ts_interfaces/interfaces.ts +46 -11
- package/ts_interfaces/requests.ts +23 -30
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.browserviewtransport.ts +532 -0
- package/ts_web/classes.controllersocketclient.ts +119 -29
- package/ts_web/elements.harnesscontrollerapp.ts +1727 -287
- package/ts_web/index.ts +1 -0
- 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,
|
package/ts/classes.validation.ts
CHANGED
|
@@ -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 >
|
|
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 {
|
|
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 {
|
|
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
|
-
):
|
|
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'] => {
|
package/ts/interfaces.flexipc.ts
CHANGED
|
@@ -32,8 +32,10 @@ export type TFlexTurnReversionCaptureInspectionV2 =
|
|
|
32
32
|
export type TFlexTurnReversionApplyInspection =
|
|
33
33
|
plugins.flexharness.IFlexTurnReversionApplyInspection;
|
|
34
34
|
|
|
35
|
-
export const flexIpcProtocolVersion =
|
|
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,
|
|
1284
|
+
&& isBoundedString(valueArg.text, textByteLimitArg, true);
|
|
1253
1285
|
case 'reasoning':
|
|
1254
1286
|
return hasFlexExactKeys(valueArg, ['partId', 'type', 'text', 'status'])
|
|
1255
|
-
&& isBoundedString(valueArg.text,
|
|
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
|
+
};
|
package/ts_interfaces/index.ts
CHANGED