@modelprofile.com/flexharness 3.6.0 → 3.7.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 +9 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +59 -1
- package/dist_ts/classes.flexharness.js +1805 -37
- package/dist_ts/classes.stores.js +1 -1
- package/dist_ts/errors.d.ts +8 -0
- package/dist_ts/errors.js +17 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +188 -5
- package/dist_ts/interfaces.js +14 -2
- package/dist_ts/plugins.d.ts +2 -2
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.json.d.ts +4 -2
- package/dist_ts/utils.json.js +253 -10
- package/dist_ts/utils.slashcommands.d.ts +7 -0
- package/dist_ts/utils.slashcommands.js +164 -0
- package/dist_ts_migration/v3_legacyflexharness.js +6 -2
- package/package.json +1 -1
- package/readme.hints.md +3 -0
- package/readme.md +114 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +2706 -245
- package/ts/classes.stores.ts +9 -8
- package/ts/errors.ts +25 -0
- package/ts/index.ts +4 -0
- package/ts/interfaces.ts +259 -5
- package/ts/plugins.ts +2 -0
- package/ts/utils.json.ts +284 -12
- package/ts/utils.slashcommands.ts +178 -0
- package/ts_migration/v3_legacyflexharness.ts +10 -5
package/ts/utils.json.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { FlexHarnessStoreFormatError, FlexHarnessValidationError } from './errors.js';
|
|
2
|
+
import {
|
|
3
|
+
FLEX_REVERSION_MAXIMUM_LIMITS,
|
|
4
|
+
FLEX_REVERSION_REFERENCE_MAX_BYTES,
|
|
5
|
+
} from './interfaces.js';
|
|
2
6
|
import type {
|
|
3
7
|
IFlexJsonLimits,
|
|
4
8
|
IFlexPermissionSnapshot,
|
|
5
|
-
|
|
9
|
+
IFlexProjectionSnapshotV1,
|
|
10
|
+
IFlexProjectionSnapshotV2,
|
|
6
11
|
IFlexScopeSnapshot,
|
|
7
12
|
TFlexAgentModelMessage,
|
|
8
13
|
TJsonValue,
|
|
@@ -390,8 +395,8 @@ function requireString(value: unknown, path: string): string {
|
|
|
390
395
|
return value;
|
|
391
396
|
}
|
|
392
397
|
|
|
393
|
-
function requireOptionalString(value: unknown, path: string):
|
|
394
|
-
|
|
398
|
+
function requireOptionalString(value: unknown, path: string): string | undefined {
|
|
399
|
+
return value === undefined ? undefined : requireString(value, path);
|
|
395
400
|
}
|
|
396
401
|
|
|
397
402
|
function requireNonNegativeNumber(value: unknown, path: string): void {
|
|
@@ -873,15 +878,12 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
|
|
|
873
878
|
}
|
|
874
879
|
}
|
|
875
880
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
'messages',
|
|
883
|
-
'stagedTerminals',
|
|
884
|
-
]);
|
|
881
|
+
interface IValidatedProjectionBody {
|
|
882
|
+
messages: IValidatedMessageIdentity[];
|
|
883
|
+
stagedUsers: IValidatedMessageIdentity[];
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function validateProjectionBody(snapshot: Record<string, unknown>): IValidatedProjectionBody {
|
|
885
887
|
const visibleMessages = validateMessages(snapshot.messages, '$snapshot.messages');
|
|
886
888
|
let projectionSessionId = visibleMessages[0]?.sessionId;
|
|
887
889
|
if (visibleMessages.some((message) => message.sessionId !== projectionSessionId)) {
|
|
@@ -892,6 +894,7 @@ export function assertFlexProjectionSnapshot(
|
|
|
892
894
|
}
|
|
893
895
|
const runIds = new Set<string>();
|
|
894
896
|
const stagedMessageIds = new Set<string>();
|
|
897
|
+
const stagedUsers: IValidatedMessageIdentity[] = [];
|
|
895
898
|
for (let index = 0; index < snapshot.stagedTerminals.length; index++) {
|
|
896
899
|
const path = `$snapshot.stagedTerminals[${index}]`;
|
|
897
900
|
const terminal = requireRecord(snapshot.stagedTerminals[index], path);
|
|
@@ -928,6 +931,10 @@ export function assertFlexProjectionSnapshot(
|
|
|
928
931
|
if (assistant.status !== terminal.status) {
|
|
929
932
|
throw new FlexHarnessStoreFormatError(`${path}.status does not match its assistant message.`);
|
|
930
933
|
}
|
|
934
|
+
if (user.status !== terminal.status) {
|
|
935
|
+
throw new FlexHarnessStoreFormatError(`${path}.status does not match its user message.`);
|
|
936
|
+
}
|
|
937
|
+
stagedUsers.push(user);
|
|
931
938
|
projectionSessionId ??= user.sessionId;
|
|
932
939
|
if (user.sessionId !== projectionSessionId) {
|
|
933
940
|
throw new FlexHarnessStoreFormatError(`${path} belongs to another session.`);
|
|
@@ -945,6 +952,271 @@ export function assertFlexProjectionSnapshot(
|
|
|
945
952
|
requireOptionalString(terminal.finishReason, `${path}.finishReason`);
|
|
946
953
|
if (terminal.steps !== undefined) requireNonNegativeInteger(terminal.steps, `${path}.steps`);
|
|
947
954
|
}
|
|
955
|
+
return { messages: visibleMessages, stagedUsers };
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export function assertFlexProjectionSnapshotV1(
|
|
959
|
+
value: unknown,
|
|
960
|
+
): asserts value is IFlexProjectionSnapshotV1 {
|
|
961
|
+
const snapshot = validateSnapshotHeader(value, [
|
|
962
|
+
'schemaVersion',
|
|
963
|
+
'revision',
|
|
964
|
+
'messages',
|
|
965
|
+
'stagedTerminals',
|
|
966
|
+
]);
|
|
967
|
+
validateProjectionBody(snapshot);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export function assertFlexProjectionSnapshotV2(
|
|
971
|
+
value: unknown,
|
|
972
|
+
): asserts value is IFlexProjectionSnapshotV2 {
|
|
973
|
+
assertJsonSerializable(value, '$snapshot');
|
|
974
|
+
const snapshot = requireRecord(value, '$snapshot');
|
|
975
|
+
requireOnlyKeys(snapshot, [
|
|
976
|
+
'schemaVersion',
|
|
977
|
+
'revision',
|
|
978
|
+
'messages',
|
|
979
|
+
'stagedTerminals',
|
|
980
|
+
'reversionSegments',
|
|
981
|
+
'revertCursor',
|
|
982
|
+
'excludedRunIds',
|
|
983
|
+
'pendingReversion',
|
|
984
|
+
'pendingReversionReleases',
|
|
985
|
+
], '$snapshot');
|
|
986
|
+
if (snapshot.schemaVersion !== 2) {
|
|
987
|
+
throw new FlexHarnessStoreFormatError('Projection snapshot schemaVersion must be 2.');
|
|
988
|
+
}
|
|
989
|
+
requireNonNegativeInteger(snapshot.revision, 'Snapshot revision');
|
|
990
|
+
const projectionBody = validateProjectionBody(snapshot);
|
|
991
|
+
const segments = Array.isArray(snapshot.reversionSegments)
|
|
992
|
+
? snapshot.reversionSegments
|
|
993
|
+
: (() => { throw new FlexHarnessStoreFormatError('Snapshot reversionSegments must be an array.'); })();
|
|
994
|
+
if (segments.length > FLEX_REVERSION_MAXIMUM_LIMITS.maxSegments) {
|
|
995
|
+
throw new FlexHarnessStoreFormatError('Snapshot reversionSegments exceeds its hard limit.');
|
|
996
|
+
}
|
|
997
|
+
const segmentRunIds = new Set<string>();
|
|
998
|
+
const segmentCaptureIds = new Set<string>();
|
|
999
|
+
const segmentEventIds = new Set<string>();
|
|
1000
|
+
const capturingWorkspaceSegments = new Map<string, string>();
|
|
1001
|
+
for (let index = 0; index < segments.length; index++) {
|
|
1002
|
+
const path = `$snapshot.reversionSegments[${index}]`;
|
|
1003
|
+
const segment = requireRecord(segments[index], path);
|
|
1004
|
+
requireOnlyKeys(segment, [
|
|
1005
|
+
'runId',
|
|
1006
|
+
'userMessageId',
|
|
1007
|
+
'status',
|
|
1008
|
+
'contextAvailable',
|
|
1009
|
+
'eventIds',
|
|
1010
|
+
'workspaceCaptured',
|
|
1011
|
+
'captureId',
|
|
1012
|
+
'workspaceReference',
|
|
1013
|
+
], path);
|
|
1014
|
+
const runId = requireString(segment.runId, `${path}.runId`);
|
|
1015
|
+
const userMessageId = requireString(segment.userMessageId, `${path}.userMessageId`);
|
|
1016
|
+
if (segmentRunIds.has(runId)) {
|
|
1017
|
+
throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate reversion run "${runId}".`);
|
|
1018
|
+
}
|
|
1019
|
+
segmentRunIds.add(runId);
|
|
1020
|
+
if (!['capturing', 'completed', 'failed', 'cancelled'].includes(String(segment.status))) {
|
|
1021
|
+
throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
|
|
1022
|
+
}
|
|
1023
|
+
if (typeof segment.contextAvailable !== 'boolean') {
|
|
1024
|
+
throw new FlexHarnessStoreFormatError(`${path}.contextAvailable must be a boolean.`);
|
|
1025
|
+
}
|
|
1026
|
+
const eventIds = requireStringArray(segment.eventIds, `${path}.eventIds`);
|
|
1027
|
+
if (eventIds.some((id) => !id) || new Set(eventIds).size !== eventIds.length) {
|
|
1028
|
+
throw new FlexHarnessStoreFormatError(`${path}.eventIds is invalid.`);
|
|
1029
|
+
}
|
|
1030
|
+
for (const eventId of eventIds) {
|
|
1031
|
+
if (segmentEventIds.has(eventId)) {
|
|
1032
|
+
throw new FlexHarnessStoreFormatError(`Snapshot reuses reversion event "${eventId}".`);
|
|
1033
|
+
}
|
|
1034
|
+
segmentEventIds.add(eventId);
|
|
1035
|
+
}
|
|
1036
|
+
if (typeof segment.workspaceCaptured !== 'boolean') {
|
|
1037
|
+
throw new FlexHarnessStoreFormatError(`${path}.workspaceCaptured must be a boolean.`);
|
|
1038
|
+
}
|
|
1039
|
+
const captureId = requireOptionalString(segment.captureId, `${path}.captureId`);
|
|
1040
|
+
if (segment.workspaceCaptured && captureId === undefined) {
|
|
1041
|
+
throw new FlexHarnessStoreFormatError(`${path} is capture-backed without a capture ID.`);
|
|
1042
|
+
}
|
|
1043
|
+
if (captureId !== undefined) {
|
|
1044
|
+
if (segmentCaptureIds.has(captureId)) {
|
|
1045
|
+
throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate capture "${captureId}".`);
|
|
1046
|
+
}
|
|
1047
|
+
segmentCaptureIds.add(captureId);
|
|
1048
|
+
if (segment.status === 'capturing') capturingWorkspaceSegments.set(runId, captureId);
|
|
1049
|
+
}
|
|
1050
|
+
if (!segment.workspaceCaptured && (captureId !== undefined || segment.workspaceReference !== undefined)) {
|
|
1051
|
+
throw new FlexHarnessStoreFormatError(`${path} is transcript-only but contains workspace capture data.`);
|
|
1052
|
+
}
|
|
1053
|
+
if (
|
|
1054
|
+
segment.workspaceCaptured
|
|
1055
|
+
&& segment.status !== 'capturing'
|
|
1056
|
+
&& segment.workspaceReference === undefined
|
|
1057
|
+
) {
|
|
1058
|
+
throw new FlexHarnessStoreFormatError(`${path} is terminal but has no workspace reference.`);
|
|
1059
|
+
}
|
|
1060
|
+
if (segment.status === 'capturing' && segment.workspaceReference !== undefined) {
|
|
1061
|
+
throw new FlexHarnessStoreFormatError(`${path} is capturing but already has a workspace reference.`);
|
|
1062
|
+
}
|
|
1063
|
+
if (segment.workspaceReference !== undefined) {
|
|
1064
|
+
if (captureId === undefined) {
|
|
1065
|
+
throw new FlexHarnessStoreFormatError(`${path} has a workspace reference without a capture ID.`);
|
|
1066
|
+
}
|
|
1067
|
+
if (Buffer.byteLength(JSON.stringify(segment.workspaceReference)) > FLEX_REVERSION_REFERENCE_MAX_BYTES) {
|
|
1068
|
+
throw new FlexHarnessStoreFormatError(`${path}.workspaceReference exceeds its byte limit.`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const correlatedUsers = new Set([...projectionBody.messages, ...projectionBody.stagedUsers]
|
|
1072
|
+
.filter((message) =>
|
|
1073
|
+
message.role === 'user' && message.runId === runId && message.messageId === userMessageId)
|
|
1074
|
+
.map((message) => `${message.runId}:${message.messageId}`));
|
|
1075
|
+
if (correlatedUsers.size !== 1) {
|
|
1076
|
+
throw new FlexHarnessStoreFormatError(`${path} does not match exactly one projected user message.`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
requireNonNegativeInteger(snapshot.revertCursor, '$snapshot.revertCursor');
|
|
1080
|
+
const candidateCount = segments.filter((segment) => {
|
|
1081
|
+
const record = segment as Record<string, unknown>;
|
|
1082
|
+
return record.status === 'completed';
|
|
1083
|
+
}).length;
|
|
1084
|
+
if (candidateCount > FLEX_REVERSION_MAXIMUM_LIMITS.maxCompletedTurns) {
|
|
1085
|
+
throw new FlexHarnessStoreFormatError('Snapshot completed reversion turns exceed their hard limit.');
|
|
1086
|
+
}
|
|
1087
|
+
if (Number(snapshot.revertCursor) > candidateCount) {
|
|
1088
|
+
throw new FlexHarnessStoreFormatError('Snapshot revertCursor exceeds its candidate count.');
|
|
1089
|
+
}
|
|
1090
|
+
const excludedRunIds = requireStringArray(snapshot.excludedRunIds, '$snapshot.excludedRunIds');
|
|
1091
|
+
if (
|
|
1092
|
+
excludedRunIds.some((id) => !id)
|
|
1093
|
+
|| new Set(excludedRunIds).size !== excludedRunIds.length
|
|
1094
|
+
) throw new FlexHarnessStoreFormatError('Snapshot excludedRunIds is invalid.');
|
|
1095
|
+
if (excludedRunIds.length > FLEX_REVERSION_MAXIMUM_LIMITS.maxExcludedRunIds) {
|
|
1096
|
+
throw new FlexHarnessStoreFormatError('Snapshot excludedRunIds exceeds its hard limit.');
|
|
1097
|
+
}
|
|
1098
|
+
if (snapshot.pendingReversion !== undefined) {
|
|
1099
|
+
const pending = requireRecord(snapshot.pendingReversion, '$snapshot.pendingReversion');
|
|
1100
|
+
if (pending.kind === 'capture') {
|
|
1101
|
+
requireOnlyKeys(pending, ['kind', 'runId', 'captureId', 'state'], '$snapshot.pendingReversion');
|
|
1102
|
+
const pendingRunId = requireString(pending.runId, '$snapshot.pendingReversion.runId');
|
|
1103
|
+
const pendingCaptureId = requireString(pending.captureId, '$snapshot.pendingReversion.captureId');
|
|
1104
|
+
if (!['preparing', 'prepared', 'finalizing'].includes(String(pending.state))) {
|
|
1105
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending capture state is invalid.');
|
|
1106
|
+
}
|
|
1107
|
+
const matches = segments.filter((segment) => {
|
|
1108
|
+
const record = segment as Record<string, unknown>;
|
|
1109
|
+
return record.runId === pendingRunId
|
|
1110
|
+
&& record.captureId === pendingCaptureId
|
|
1111
|
+
&& record.workspaceCaptured === true
|
|
1112
|
+
&& record.status === 'capturing';
|
|
1113
|
+
});
|
|
1114
|
+
if (matches.length !== 1) {
|
|
1115
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending capture does not match exactly one segment.');
|
|
1116
|
+
}
|
|
1117
|
+
capturingWorkspaceSegments.delete(pendingRunId);
|
|
1118
|
+
} else if (pending.kind === 'apply') {
|
|
1119
|
+
requireOnlyKeys(pending, [
|
|
1120
|
+
'kind',
|
|
1121
|
+
'operationId',
|
|
1122
|
+
'direction',
|
|
1123
|
+
'fromCursor',
|
|
1124
|
+
'toCursor',
|
|
1125
|
+
'segmentRunIds',
|
|
1126
|
+
'appliedRunIds',
|
|
1127
|
+
], '$snapshot.pendingReversion');
|
|
1128
|
+
requireString(pending.operationId, '$snapshot.pendingReversion.operationId');
|
|
1129
|
+
if (!['undo', 'redo'].includes(String(pending.direction))) {
|
|
1130
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending apply direction is invalid.');
|
|
1131
|
+
}
|
|
1132
|
+
requireNonNegativeInteger(pending.fromCursor, '$snapshot.pendingReversion.fromCursor');
|
|
1133
|
+
requireNonNegativeInteger(pending.toCursor, '$snapshot.pendingReversion.toCursor');
|
|
1134
|
+
const pendingRuns = requireStringArray(
|
|
1135
|
+
pending.segmentRunIds,
|
|
1136
|
+
'$snapshot.pendingReversion.segmentRunIds',
|
|
1137
|
+
);
|
|
1138
|
+
const appliedRuns = requireStringArray(
|
|
1139
|
+
pending.appliedRunIds,
|
|
1140
|
+
'$snapshot.pendingReversion.appliedRunIds',
|
|
1141
|
+
);
|
|
1142
|
+
if (new Set(pendingRuns).size !== pendingRuns.length || new Set(appliedRuns).size !== appliedRuns.length) {
|
|
1143
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending apply run IDs are invalid.');
|
|
1144
|
+
}
|
|
1145
|
+
if (appliedRuns.some((runId) => !pendingRuns.includes(runId))) {
|
|
1146
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending apply progress is invalid.');
|
|
1147
|
+
}
|
|
1148
|
+
const fromCursor = Number(pending.fromCursor);
|
|
1149
|
+
const toCursor = Number(pending.toCursor);
|
|
1150
|
+
const direction = pending.direction as 'undo' | 'redo';
|
|
1151
|
+
if (fromCursor !== Number(snapshot.revertCursor) || toCursor !== fromCursor + (direction === 'undo' ? -1 : 1)) {
|
|
1152
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending apply cursor movement is invalid.');
|
|
1153
|
+
}
|
|
1154
|
+
const completed = segments.filter((segment) =>
|
|
1155
|
+
(segment as Record<string, unknown>).status === 'completed');
|
|
1156
|
+
const target = direction === 'undo' ? completed[fromCursor - 1] : completed[fromCursor];
|
|
1157
|
+
if (!target) throw new FlexHarnessStoreFormatError('Snapshot pending apply has no target candidate.');
|
|
1158
|
+
const targetRunId = (target as Record<string, unknown>).runId;
|
|
1159
|
+
const targetIndex = segments.findIndex((segment) =>
|
|
1160
|
+
(segment as Record<string, unknown>).runId === targetRunId);
|
|
1161
|
+
const start = target === completed[0] ? 0 : targetIndex;
|
|
1162
|
+
const next = completed[direction === 'undo' ? fromCursor : fromCursor + 1];
|
|
1163
|
+
const end = next
|
|
1164
|
+
? segments.findIndex((segment) =>
|
|
1165
|
+
(segment as Record<string, unknown>).runId === (next as Record<string, unknown>).runId)
|
|
1166
|
+
: segments.length;
|
|
1167
|
+
const exactRuns = segments.slice(start, end).map((segment) =>
|
|
1168
|
+
String((segment as Record<string, unknown>).runId));
|
|
1169
|
+
if (JSON.stringify(pendingRuns) !== JSON.stringify(exactRuns)) {
|
|
1170
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending apply segment unit is invalid.');
|
|
1171
|
+
}
|
|
1172
|
+
const applyOrder = direction === 'undo' ? [...exactRuns].reverse() : exactRuns;
|
|
1173
|
+
if (JSON.stringify(appliedRuns) !== JSON.stringify(applyOrder.slice(0, appliedRuns.length))) {
|
|
1174
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending apply progress is out of order.');
|
|
1175
|
+
}
|
|
1176
|
+
} else {
|
|
1177
|
+
throw new FlexHarnessStoreFormatError('Snapshot pendingReversion kind is invalid.');
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (capturingWorkspaceSegments.size > 0) {
|
|
1181
|
+
throw new FlexHarnessStoreFormatError(
|
|
1182
|
+
'Snapshot contains a capture-backed capturing segment without exact pending ownership.',
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
if (!Array.isArray(snapshot.pendingReversionReleases)) {
|
|
1186
|
+
throw new FlexHarnessStoreFormatError('Snapshot pendingReversionReleases must be an array.');
|
|
1187
|
+
}
|
|
1188
|
+
if (
|
|
1189
|
+
snapshot.pendingReversionReleases.length
|
|
1190
|
+
> FLEX_REVERSION_MAXIMUM_LIMITS.maxPendingReversionReleases
|
|
1191
|
+
) {
|
|
1192
|
+
throw new FlexHarnessStoreFormatError('Snapshot pending reversion releases exceed their hard limit.');
|
|
1193
|
+
}
|
|
1194
|
+
const releaseCaptureIds = new Set<string>();
|
|
1195
|
+
for (let index = 0; index < snapshot.pendingReversionReleases.length; index++) {
|
|
1196
|
+
const path = `$snapshot.pendingReversionReleases[${index}]`;
|
|
1197
|
+
const release = requireRecord(snapshot.pendingReversionReleases[index], path);
|
|
1198
|
+
requireOnlyKeys(release, ['runId', 'captureId', 'reference'], path);
|
|
1199
|
+
requireString(release.runId, `${path}.runId`);
|
|
1200
|
+
const captureId = requireString(release.captureId, `${path}.captureId`);
|
|
1201
|
+
if (releaseCaptureIds.has(captureId)) {
|
|
1202
|
+
throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate release capture "${captureId}".`);
|
|
1203
|
+
}
|
|
1204
|
+
releaseCaptureIds.add(captureId);
|
|
1205
|
+
if (!Object.prototype.hasOwnProperty.call(release, 'reference')) {
|
|
1206
|
+
throw new FlexHarnessStoreFormatError(`${path}.reference is required.`);
|
|
1207
|
+
}
|
|
1208
|
+
if (Buffer.byteLength(JSON.stringify(release.reference)) > FLEX_REVERSION_REFERENCE_MAX_BYTES) {
|
|
1209
|
+
throw new FlexHarnessStoreFormatError(`${path}.reference exceeds its byte limit.`);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export function assertFlexProjectionSnapshot(
|
|
1215
|
+
value: unknown,
|
|
1216
|
+
): asserts value is IFlexProjectionSnapshotV1 | IFlexProjectionSnapshotV2 {
|
|
1217
|
+
const record = requireRecord(value, '$snapshot');
|
|
1218
|
+
if (record.schemaVersion === 1) assertFlexProjectionSnapshotV1(value);
|
|
1219
|
+
else assertFlexProjectionSnapshotV2(value);
|
|
948
1220
|
}
|
|
949
1221
|
|
|
950
1222
|
export function assertFlexPermissionSnapshot(
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { FlexHarnessValidationError } from './errors.js';
|
|
2
|
+
import type {
|
|
3
|
+
IFlexParsedSlashCommand,
|
|
4
|
+
TFlexSlashCommandParseResult,
|
|
5
|
+
} from './interfaces.js';
|
|
6
|
+
|
|
7
|
+
export const FLEX_SLASH_COMMAND_MAX_INPUT_BYTES = 768 * 1024;
|
|
8
|
+
|
|
9
|
+
const slashCommandNamePattern = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
10
|
+
const slashCommandPattern = /^\/([a-z][a-z0-9-]{0,63})(?:$|(\s+)([\s\S]*))$/u;
|
|
11
|
+
const slashCommandArgumentsPattern = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi;
|
|
12
|
+
const slashCommandQuoteTrimPattern = /^["']|["']$/g;
|
|
13
|
+
const slashCommandPlaceholderPattern = /\$(\d+)/g;
|
|
14
|
+
const slashCommandAllPlaceholderPattern = /\$ARGUMENTS|\$(\d+)/g;
|
|
15
|
+
|
|
16
|
+
export const FLEX_SLASH_COMMAND_INITIALIZE_TEMPLATE = `Create or update \`AGENTS.md\` for this repository.
|
|
17
|
+
|
|
18
|
+
The goal is a compact instruction file that helps future OpenCode sessions avoid mistakes and ramp up quickly. Every line should answer: "Would an agent likely miss this without help?" If not, leave it out.
|
|
19
|
+
|
|
20
|
+
User-provided focus or constraints (honor these):
|
|
21
|
+
$ARGUMENTS
|
|
22
|
+
|
|
23
|
+
## How to investigate
|
|
24
|
+
|
|
25
|
+
Read the highest-value sources first:
|
|
26
|
+
- \`README*\`, root manifests, workspace config, lockfiles
|
|
27
|
+
- build, test, lint, formatter, typecheck, and codegen config
|
|
28
|
+
- CI workflows and pre-commit / task runner config
|
|
29
|
+
- existing instruction files (\`AGENTS.md\`, \`CLAUDE.md\`, \`.cursor/rules/\`, \`.cursorrules\`, \`.github/copilot-instructions.md\`)
|
|
30
|
+
- repo-local OpenCode config such as \`opencode.json\`
|
|
31
|
+
|
|
32
|
+
If architecture is still unclear after reading config and docs, inspect a small number of representative code files to find the real entrypoints, package boundaries, and execution flow. Prefer reading the files that explain how the system is wired together over random leaf files.
|
|
33
|
+
|
|
34
|
+
Prefer executable sources of truth over prose. If docs conflict with config or scripts, trust the executable source and only keep what you can verify.
|
|
35
|
+
|
|
36
|
+
## What to extract
|
|
37
|
+
|
|
38
|
+
Look for the highest-signal facts for an agent working in this repo:
|
|
39
|
+
- exact developer commands, especially non-obvious ones
|
|
40
|
+
- how to run a single test, a single package, or a focused verification step
|
|
41
|
+
- required command order when it matters, such as \`lint -> typecheck -> test\`
|
|
42
|
+
- monorepo or multi-package boundaries, ownership of major directories, and the real app/library entrypoints
|
|
43
|
+
- framework or toolchain quirks: generated code, migrations, codegen, build artifacts, special env loading, dev servers, infra deploy flow
|
|
44
|
+
- repo-specific style or workflow conventions that differ from defaults
|
|
45
|
+
- testing quirks: fixtures, integration test prerequisites, snapshot workflows, required services, flaky or expensive suites
|
|
46
|
+
- important constraints from existing instruction files worth preserving
|
|
47
|
+
|
|
48
|
+
Good \`AGENTS.md\` content is usually hard-earned context that took reading multiple files to infer.
|
|
49
|
+
|
|
50
|
+
## Questions
|
|
51
|
+
|
|
52
|
+
Only ask the user questions if the repo cannot answer something important. Use the \`question\` tool for one short batch at most.
|
|
53
|
+
|
|
54
|
+
Good questions:
|
|
55
|
+
- undocumented team conventions
|
|
56
|
+
- branch / PR / release expectations
|
|
57
|
+
- missing setup or test prerequisites that are known but not written down
|
|
58
|
+
|
|
59
|
+
Do not ask about anything the repo already makes clear.
|
|
60
|
+
|
|
61
|
+
## Writing rules
|
|
62
|
+
|
|
63
|
+
Include only high-signal, repo-specific guidance such as:
|
|
64
|
+
- exact commands and shortcuts the agent would otherwise guess wrong
|
|
65
|
+
- architecture notes that are not obvious from filenames
|
|
66
|
+
- conventions that differ from language or framework defaults
|
|
67
|
+
- setup requirements, environment quirks, and operational gotchas
|
|
68
|
+
- references to existing instruction sources that matter
|
|
69
|
+
|
|
70
|
+
Exclude:
|
|
71
|
+
- generic software advice
|
|
72
|
+
- long tutorials or exhaustive file trees
|
|
73
|
+
- obvious language conventions
|
|
74
|
+
- speculative claims or anything you could not verify
|
|
75
|
+
- content better stored in another file referenced via \`opencode.json\` \`instructions\`
|
|
76
|
+
|
|
77
|
+
When in doubt, omit.
|
|
78
|
+
|
|
79
|
+
Prefer short sections and bullets. If the repo is simple, keep the file simple. If the repo is large, summarize the few structural facts that actually change how an agent should work.
|
|
80
|
+
|
|
81
|
+
If \`AGENTS.md\` already exists in the active workspace, improve it in place rather than rewriting blindly. Preserve verified useful guidance, delete fluff or stale claims, and reconcile it with the current codebase.
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export function isValidSlashCommandName(name: string): boolean {
|
|
85
|
+
return slashCommandNamePattern.test(name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function tokenizeSlashCommandArguments(rawArguments: string): string[] {
|
|
89
|
+
const tokens = rawArguments.match(slashCommandArgumentsPattern) ?? [];
|
|
90
|
+
return tokens.map((token) => token.replace(slashCommandQuoteTrimPattern, ''));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function parseSlashCommand(input: string): TFlexSlashCommandParseResult {
|
|
94
|
+
if (typeof input !== 'string') {
|
|
95
|
+
throw new FlexHarnessValidationError('Slash command input must be a string.');
|
|
96
|
+
}
|
|
97
|
+
if (!input.startsWith('/')) return Object.freeze({ type: 'not-command' });
|
|
98
|
+
if (Buffer.byteLength(input, 'utf8') > FLEX_SLASH_COMMAND_MAX_INPUT_BYTES) {
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
type: 'malformed',
|
|
101
|
+
reason: `Slash command input exceeds ${FLEX_SLASH_COMMAND_MAX_INPUT_BYTES} UTF-8 bytes.`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const match = slashCommandPattern.exec(input);
|
|
105
|
+
if (!match) {
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
type: 'malformed',
|
|
108
|
+
reason: 'Slash command syntax is invalid.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const arguments_ = tokenizeSlashCommandArguments(match[3] ?? '');
|
|
112
|
+
Object.freeze(arguments_);
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
type: 'parsed',
|
|
115
|
+
input,
|
|
116
|
+
name: match[1],
|
|
117
|
+
rawArguments: match[3] ?? '',
|
|
118
|
+
arguments: arguments_,
|
|
119
|
+
} satisfies IFlexParsedSlashCommand);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function slashCommandTemplateHints(template: string): string[] {
|
|
123
|
+
const hints = [...new Set(template.match(/\$\d+/g) ?? [])].sort();
|
|
124
|
+
if (template.includes('$ARGUMENTS')) hints.push('$ARGUMENTS');
|
|
125
|
+
return hints;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function expandSlashCommandTemplate(
|
|
129
|
+
template: string,
|
|
130
|
+
rawArguments: string,
|
|
131
|
+
arguments_: readonly string[],
|
|
132
|
+
): string {
|
|
133
|
+
const placeholders = template.match(slashCommandPlaceholderPattern) ?? [];
|
|
134
|
+
let highestPosition = 0;
|
|
135
|
+
for (const placeholder of placeholders) {
|
|
136
|
+
highestPosition = Math.max(highestPosition, Number(placeholder.slice(1)));
|
|
137
|
+
}
|
|
138
|
+
const segments: string[] = [];
|
|
139
|
+
let lastIndex = 0;
|
|
140
|
+
let expandedBytes = 0;
|
|
141
|
+
for (const match of template.matchAll(slashCommandAllPlaceholderPattern)) {
|
|
142
|
+
const literal = template.slice(lastIndex, match.index);
|
|
143
|
+
let replacement: string;
|
|
144
|
+
if (match[0] === '$ARGUMENTS') {
|
|
145
|
+
replacement = rawArguments;
|
|
146
|
+
} else {
|
|
147
|
+
const position = Number(match[1]);
|
|
148
|
+
const argumentIndex = position - 1;
|
|
149
|
+
replacement = argumentIndex < 0 || argumentIndex >= arguments_.length
|
|
150
|
+
? ''
|
|
151
|
+
: position === highestPosition
|
|
152
|
+
? arguments_.slice(argumentIndex).join(' ')
|
|
153
|
+
: arguments_[argumentIndex];
|
|
154
|
+
}
|
|
155
|
+
expandedBytes += Buffer.byteLength(literal, 'utf8') + Buffer.byteLength(replacement, 'utf8');
|
|
156
|
+
if (expandedBytes > FLEX_SLASH_COMMAND_MAX_INPUT_BYTES) {
|
|
157
|
+
throw new FlexHarnessValidationError('Expanded slash command prompt exceeds the input limit.');
|
|
158
|
+
}
|
|
159
|
+
segments.push(literal, replacement);
|
|
160
|
+
lastIndex = match.index + match[0].length;
|
|
161
|
+
}
|
|
162
|
+
segments.push(template.slice(lastIndex));
|
|
163
|
+
expandedBytes += Buffer.byteLength(segments.at(-1)!, 'utf8');
|
|
164
|
+
if (expandedBytes > FLEX_SLASH_COMMAND_MAX_INPUT_BYTES) {
|
|
165
|
+
throw new FlexHarnessValidationError('Expanded slash command prompt exceeds the input limit.');
|
|
166
|
+
}
|
|
167
|
+
let expanded = segments.join('');
|
|
168
|
+
if (placeholders.length === 0 && !template.includes('$ARGUMENTS') && rawArguments.trim()) {
|
|
169
|
+
if (
|
|
170
|
+
expandedBytes + Buffer.byteLength(rawArguments, 'utf8') + 2
|
|
171
|
+
> FLEX_SLASH_COMMAND_MAX_INPUT_BYTES
|
|
172
|
+
) {
|
|
173
|
+
throw new FlexHarnessValidationError('Expanded slash command prompt exceeds the input limit.');
|
|
174
|
+
}
|
|
175
|
+
expanded += `\n\n${rawArguments}`;
|
|
176
|
+
}
|
|
177
|
+
return expanded;
|
|
178
|
+
}
|
|
@@ -4,10 +4,11 @@ import type {
|
|
|
4
4
|
IFlexHarnessStores,
|
|
5
5
|
IFlexMessage,
|
|
6
6
|
IFlexPermissionSnapshot,
|
|
7
|
-
|
|
7
|
+
IFlexProjectionSnapshotCurrent,
|
|
8
8
|
IFlexScopeSnapshot,
|
|
9
9
|
IFlexSession,
|
|
10
10
|
TFlexAgentModelMessage,
|
|
11
|
+
TFlexProjectionSnapshot,
|
|
11
12
|
TJsonValue,
|
|
12
13
|
} from '../ts/interfaces.js';
|
|
13
14
|
import {
|
|
@@ -44,7 +45,7 @@ interface ILegacyRun {
|
|
|
44
45
|
|
|
45
46
|
interface ILegacySessionMigrationPlan {
|
|
46
47
|
sessionId: string;
|
|
47
|
-
projectionSnapshot:
|
|
48
|
+
projectionSnapshot: IFlexProjectionSnapshotCurrent;
|
|
48
49
|
permissionSnapshot: IFlexPermissionSnapshot;
|
|
49
50
|
agentEvents: plugins.TAgentEvent[];
|
|
50
51
|
}
|
|
@@ -56,7 +57,7 @@ interface ILegacyMigrationPlan {
|
|
|
56
57
|
|
|
57
58
|
interface IInspectedSessionDestination {
|
|
58
59
|
plan: ILegacySessionMigrationPlan;
|
|
59
|
-
projection:
|
|
60
|
+
projection: TFlexProjectionSnapshot | undefined;
|
|
60
61
|
permission: IFlexPermissionSnapshot | undefined;
|
|
61
62
|
eventStore: plugins.IAgentEventStoreV2;
|
|
62
63
|
eventSnapshot: plugins.IAgentEventSnapshotV2 | undefined;
|
|
@@ -403,11 +404,15 @@ function createMigrationPlan(
|
|
|
403
404
|
sessions: [stored.session],
|
|
404
405
|
tombstones: [],
|
|
405
406
|
};
|
|
406
|
-
const projectionSnapshot:
|
|
407
|
-
schemaVersion:
|
|
407
|
+
const projectionSnapshot: IFlexProjectionSnapshotCurrent = {
|
|
408
|
+
schemaVersion: 2,
|
|
408
409
|
revision: 1,
|
|
409
410
|
messages: stored.messages,
|
|
410
411
|
stagedTerminals: [],
|
|
412
|
+
reversionSegments: [],
|
|
413
|
+
revertCursor: 0,
|
|
414
|
+
excludedRunIds: [],
|
|
415
|
+
pendingReversionReleases: [],
|
|
411
416
|
};
|
|
412
417
|
const permissionSnapshot: IFlexPermissionSnapshot = {
|
|
413
418
|
schemaVersion: 1,
|