@modelprofile.com/flexharness 2.1.0 → 3.0.1
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 +66 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +86 -30
- package/dist_ts/classes.flexharness.js +2593 -1131
- package/dist_ts/classes.stores.d.ts +19 -20
- package/dist_ts/classes.stores.js +968 -75
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +11 -1
- package/dist_ts/interfaces.d.ts +130 -23
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.json.d.ts +4 -2
- package/dist_ts/utils.json.js +283 -140
- package/dist_ts/utils.prompt.d.ts +1 -2
- package/dist_ts/utils.prompt.js +3 -26
- package/dist_ts_migration/index.d.ts +1 -0
- package/dist_ts_migration/index.js +2 -0
- package/dist_ts_migration/v3_legacyflexharness.d.ts +17 -0
- package/dist_ts_migration/v3_legacyflexharness.js +426 -0
- package/package.json +17 -3
- package/readme.hints.md +13 -11
- package/readme.md +94 -27
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +2966 -1421
- package/ts/classes.stores.ts +1226 -97
- package/ts/errors.ts +20 -0
- package/ts/interfaces.ts +168 -22
- package/ts/plugins.ts +46 -3
- package/ts/utils.json.ts +341 -154
- package/ts/utils.prompt.ts +6 -27
- package/ts_migration/index.ts +1 -0
- package/ts_migration/v3_legacyflexharness.ts +620 -0
package/ts/utils.json.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { FlexHarnessStoreFormatError, FlexHarnessValidationError } from './errors.js';
|
|
2
2
|
import type {
|
|
3
|
-
IFlexHarnessSnapshot,
|
|
4
3
|
IFlexJsonLimits,
|
|
4
|
+
IFlexPermissionSnapshot,
|
|
5
|
+
IFlexProjectionSnapshot,
|
|
6
|
+
IFlexScopeSnapshot,
|
|
5
7
|
TFlexAgentModelMessage,
|
|
6
8
|
TJsonValue,
|
|
7
9
|
} from './interfaces.js';
|
|
@@ -370,6 +372,17 @@ function requireRecord(value: unknown, path: string): Record<string, unknown> {
|
|
|
370
372
|
return value as Record<string, unknown>;
|
|
371
373
|
}
|
|
372
374
|
|
|
375
|
+
function requireOnlyKeys(
|
|
376
|
+
value: Record<string, unknown>,
|
|
377
|
+
allowedKeys: readonly string[],
|
|
378
|
+
path: string,
|
|
379
|
+
): void {
|
|
380
|
+
const unexpectedKey = Object.keys(value).find((key) => !allowedKeys.includes(key));
|
|
381
|
+
if (unexpectedKey) {
|
|
382
|
+
throw new FlexHarnessStoreFormatError(`${path}.${unexpectedKey} is not supported.`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
373
386
|
function requireString(value: unknown, path: string): string {
|
|
374
387
|
if (typeof value !== 'string' || value.length === 0) {
|
|
375
388
|
throw new FlexHarnessStoreFormatError(`${path} must be a non-empty string.`);
|
|
@@ -387,6 +400,12 @@ function requireNonNegativeNumber(value: unknown, path: string): void {
|
|
|
387
400
|
}
|
|
388
401
|
}
|
|
389
402
|
|
|
403
|
+
function requireNonNegativeInteger(value: unknown, path: string): void {
|
|
404
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) {
|
|
405
|
+
throw new FlexHarnessStoreFormatError(`${path} must be a non-negative integer.`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
390
409
|
function requireStringArray(value: unknown, path: string): string[] {
|
|
391
410
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
392
411
|
throw new FlexHarnessStoreFormatError(`${path} must be an array of strings.`);
|
|
@@ -394,187 +413,355 @@ function requireStringArray(value: unknown, path: string): string[] {
|
|
|
394
413
|
return value;
|
|
395
414
|
}
|
|
396
415
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
416
|
+
const activityStatuses = [
|
|
417
|
+
'idle',
|
|
418
|
+
'scheduled',
|
|
419
|
+
'running',
|
|
420
|
+
'waiting_permission',
|
|
421
|
+
'failed',
|
|
422
|
+
'cancelled',
|
|
423
|
+
] as const;
|
|
424
|
+
|
|
425
|
+
function validateSession(value: unknown, path: string): string {
|
|
426
|
+
const session = requireRecord(value, path);
|
|
427
|
+
requireOnlyKeys(
|
|
428
|
+
session,
|
|
429
|
+
[
|
|
430
|
+
'scopeId',
|
|
431
|
+
'sessionId',
|
|
432
|
+
'title',
|
|
433
|
+
'createdAt',
|
|
434
|
+
'updatedAt',
|
|
435
|
+
'archivedAt',
|
|
436
|
+
'status',
|
|
437
|
+
'activity',
|
|
438
|
+
],
|
|
439
|
+
path,
|
|
440
|
+
);
|
|
441
|
+
requireString(session.scopeId, `${path}.scopeId`);
|
|
442
|
+
const sessionId = requireString(session.sessionId, `${path}.sessionId`);
|
|
443
|
+
requireOptionalString(session.title, `${path}.title`);
|
|
444
|
+
requireString(session.createdAt, `${path}.createdAt`);
|
|
445
|
+
requireString(session.updatedAt, `${path}.updatedAt`);
|
|
446
|
+
requireOptionalString(session.archivedAt, `${path}.archivedAt`);
|
|
447
|
+
if (!activityStatuses.includes(session.status as (typeof activityStatuses)[number])) {
|
|
448
|
+
throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
|
|
449
|
+
}
|
|
450
|
+
const activity = requireRecord(session.activity, `${path}.activity`);
|
|
451
|
+
requireOnlyKeys(activity, ['runId', 'status', 'startedAt', 'completedAt', 'error'], `${path}.activity`);
|
|
452
|
+
if (!activityStatuses.includes(activity.status as (typeof activityStatuses)[number])) {
|
|
453
|
+
throw new FlexHarnessStoreFormatError(`${path}.activity.status is invalid.`);
|
|
454
|
+
}
|
|
455
|
+
requireOptionalString(activity.runId, `${path}.activity.runId`);
|
|
456
|
+
requireOptionalString(activity.startedAt, `${path}.activity.startedAt`);
|
|
457
|
+
requireOptionalString(activity.completedAt, `${path}.activity.completedAt`);
|
|
458
|
+
requireOptionalString(activity.error, `${path}.activity.error`);
|
|
459
|
+
return sessionId;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function validateModel(value: unknown, path: string): void {
|
|
463
|
+
const model = requireRecord(value, path);
|
|
464
|
+
requireOnlyKeys(model, ['provider', 'model', 'displayName', 'variant'], path);
|
|
465
|
+
requireString(model.provider, `${path}.provider`);
|
|
466
|
+
requireString(model.model, `${path}.model`);
|
|
467
|
+
requireOptionalString(model.displayName, `${path}.displayName`);
|
|
468
|
+
requireOptionalString(model.variant, `${path}.variant`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function validateUsage(value: unknown, path: string): void {
|
|
472
|
+
const usage = requireRecord(value, path);
|
|
473
|
+
requireOnlyKeys(
|
|
474
|
+
usage,
|
|
475
|
+
['inputTokens', 'outputTokens', 'totalTokens', 'cacheReadTokens', 'cacheWriteTokens'],
|
|
476
|
+
path,
|
|
477
|
+
);
|
|
478
|
+
requireNonNegativeNumber(usage.inputTokens, `${path}.inputTokens`);
|
|
479
|
+
requireNonNegativeNumber(usage.outputTokens, `${path}.outputTokens`);
|
|
480
|
+
requireNonNegativeNumber(usage.totalTokens, `${path}.totalTokens`);
|
|
481
|
+
requireNonNegativeNumber(usage.cacheReadTokens, `${path}.cacheReadTokens`);
|
|
482
|
+
requireNonNegativeNumber(usage.cacheWriteTokens, `${path}.cacheWriteTokens`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
interface IValidatedMessageIdentity {
|
|
486
|
+
messageId: string;
|
|
487
|
+
sessionId: string;
|
|
488
|
+
runId: string;
|
|
489
|
+
role: 'user' | 'assistant';
|
|
490
|
+
status: 'streaming' | 'completed' | 'failed' | 'cancelled';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function validateMessage(value: unknown, path: string): IValidatedMessageIdentity {
|
|
494
|
+
const message = requireRecord(value, path);
|
|
495
|
+
requireOnlyKeys(
|
|
496
|
+
message,
|
|
497
|
+
[
|
|
498
|
+
'messageId',
|
|
499
|
+
'sessionId',
|
|
500
|
+
'runId',
|
|
501
|
+
'role',
|
|
502
|
+
'status',
|
|
503
|
+
'createdAt',
|
|
504
|
+
'completedAt',
|
|
505
|
+
'parts',
|
|
506
|
+
'model',
|
|
507
|
+
'usage',
|
|
508
|
+
'error',
|
|
509
|
+
],
|
|
510
|
+
path,
|
|
511
|
+
);
|
|
512
|
+
const messageId = requireString(message.messageId, `${path}.messageId`);
|
|
513
|
+
const sessionId = requireString(message.sessionId, `${path}.sessionId`);
|
|
514
|
+
const runId = requireString(message.runId, `${path}.runId`);
|
|
515
|
+
if (message.role !== 'user' && message.role !== 'assistant') {
|
|
516
|
+
throw new FlexHarnessStoreFormatError(`${path}.role is invalid.`);
|
|
517
|
+
}
|
|
518
|
+
if (!['streaming', 'completed', 'failed', 'cancelled'].includes(String(message.status))) {
|
|
519
|
+
throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
|
|
520
|
+
}
|
|
521
|
+
requireString(message.createdAt, `${path}.createdAt`);
|
|
522
|
+
requireOptionalString(message.completedAt, `${path}.completedAt`);
|
|
523
|
+
requireOptionalString(message.error, `${path}.error`);
|
|
524
|
+
if (message.model !== undefined) validateModel(message.model, `${path}.model`);
|
|
525
|
+
if (message.usage !== undefined) validateUsage(message.usage, `${path}.usage`);
|
|
526
|
+
if (!Array.isArray(message.parts)) {
|
|
527
|
+
throw new FlexHarnessStoreFormatError(`${path}.parts must be an array.`);
|
|
528
|
+
}
|
|
529
|
+
const partIds = new Set<string>();
|
|
530
|
+
for (let index = 0; index < message.parts.length; index++) {
|
|
531
|
+
const partPath = `${path}.parts[${index}]`;
|
|
532
|
+
const part = requireRecord(message.parts[index], partPath);
|
|
533
|
+
const partId = requireString(part.partId, `${partPath}.partId`);
|
|
534
|
+
if (partIds.has(partId)) {
|
|
535
|
+
throw new FlexHarnessStoreFormatError(`${path}.parts contains duplicate part "${partId}".`);
|
|
536
|
+
}
|
|
537
|
+
partIds.add(partId);
|
|
538
|
+
if (part.type === 'text') {
|
|
539
|
+
requireOnlyKeys(part, ['partId', 'type', 'text'], partPath);
|
|
540
|
+
if (typeof part.text !== 'string') {
|
|
541
|
+
throw new FlexHarnessStoreFormatError(`${partPath}.text must be a string.`);
|
|
468
542
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
543
|
+
} else if (part.type === 'reasoning') {
|
|
544
|
+
requireOnlyKeys(part, ['partId', 'type', 'text', 'status'], partPath);
|
|
545
|
+
if (typeof part.text !== 'string') {
|
|
546
|
+
throw new FlexHarnessStoreFormatError(`${partPath}.text must be a string.`);
|
|
472
547
|
}
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
'type',
|
|
500
|
-
'attachmentType',
|
|
501
|
-
'source',
|
|
502
|
-
'sizeBytes',
|
|
503
|
-
'mediaType',
|
|
504
|
-
'name',
|
|
505
|
-
].includes(key),
|
|
506
|
-
);
|
|
507
|
-
if (unsupportedAttachmentField) {
|
|
508
|
-
throw new FlexHarnessStoreFormatError(
|
|
509
|
-
`${partPath}.${unsupportedAttachmentField} is not public attachment metadata.`,
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
if (!['image', 'file'].includes(String(part.attachmentType))) {
|
|
513
|
-
throw new FlexHarnessStoreFormatError(`${partPath}.attachmentType is invalid.`);
|
|
514
|
-
}
|
|
515
|
-
if (!['inline-base64', 'data-url', 'remote-url'].includes(String(part.source))) {
|
|
516
|
-
throw new FlexHarnessStoreFormatError(`${partPath}.source is invalid.`);
|
|
517
|
-
}
|
|
518
|
-
if (
|
|
519
|
-
part.sizeBytes !== undefined &&
|
|
520
|
-
(!Number.isSafeInteger(part.sizeBytes) || Number(part.sizeBytes) < 0)
|
|
521
|
-
) {
|
|
522
|
-
throw new FlexHarnessStoreFormatError(
|
|
523
|
-
`${partPath}.sizeBytes must be a non-negative integer.`,
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
requireOptionalString(part.mediaType, `${partPath}.mediaType`);
|
|
527
|
-
requireOptionalString(part.name, `${partPath}.name`);
|
|
548
|
+
if (!['running', 'completed', 'cancelled'].includes(String(part.status))) {
|
|
549
|
+
throw new FlexHarnessStoreFormatError(`${partPath}.status is invalid.`);
|
|
550
|
+
}
|
|
551
|
+
} else if (part.type === 'tool') {
|
|
552
|
+
requireOnlyKeys(
|
|
553
|
+
part,
|
|
554
|
+
['partId', 'type', 'toolCallId', 'toolName', 'status', 'input', 'output', 'error'],
|
|
555
|
+
partPath,
|
|
556
|
+
);
|
|
557
|
+
requireString(part.toolCallId, `${partPath}.toolCallId`);
|
|
558
|
+
requireString(part.toolName, `${partPath}.toolName`);
|
|
559
|
+
if (!['running', 'completed', 'failed', 'cancelled'].includes(String(part.status))) {
|
|
560
|
+
throw new FlexHarnessStoreFormatError(`${partPath}.status is invalid.`);
|
|
561
|
+
}
|
|
562
|
+
if (!Object.prototype.hasOwnProperty.call(part, 'input')) {
|
|
563
|
+
throw new FlexHarnessStoreFormatError(`${partPath}.input is required.`);
|
|
564
|
+
}
|
|
565
|
+
requireOptionalString(part.error, `${partPath}.error`);
|
|
566
|
+
} else if (part.type === 'attachment') {
|
|
567
|
+
requireOnlyKeys(
|
|
568
|
+
part,
|
|
569
|
+
['partId', 'type', 'attachmentType', 'source', 'sizeBytes', 'mediaType', 'name'],
|
|
570
|
+
partPath,
|
|
571
|
+
);
|
|
572
|
+
if (!['image', 'file'].includes(String(part.attachmentType))) {
|
|
573
|
+
throw new FlexHarnessStoreFormatError(`${partPath}.attachmentType is invalid.`);
|
|
528
574
|
}
|
|
575
|
+
if (!['inline-base64', 'data-url', 'remote-url'].includes(String(part.source))) {
|
|
576
|
+
throw new FlexHarnessStoreFormatError(`${partPath}.source is invalid.`);
|
|
577
|
+
}
|
|
578
|
+
if (part.sizeBytes !== undefined) {
|
|
579
|
+
requireNonNegativeInteger(part.sizeBytes, `${partPath}.sizeBytes`);
|
|
580
|
+
}
|
|
581
|
+
requireOptionalString(part.mediaType, `${partPath}.mediaType`);
|
|
582
|
+
requireOptionalString(part.name, `${partPath}.name`);
|
|
583
|
+
} else {
|
|
584
|
+
throw new FlexHarnessStoreFormatError(`${partPath}.type is invalid.`);
|
|
529
585
|
}
|
|
530
586
|
}
|
|
587
|
+
return {
|
|
588
|
+
messageId,
|
|
589
|
+
sessionId,
|
|
590
|
+
runId,
|
|
591
|
+
role: message.role,
|
|
592
|
+
status: message.status as IValidatedMessageIdentity['status'],
|
|
593
|
+
};
|
|
594
|
+
}
|
|
531
595
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
596
|
+
function validateMessages(
|
|
597
|
+
value: unknown,
|
|
598
|
+
path: string,
|
|
599
|
+
expectedSessionId?: string,
|
|
600
|
+
): IValidatedMessageIdentity[] {
|
|
601
|
+
if (!Array.isArray(value)) {
|
|
602
|
+
throw new FlexHarnessStoreFormatError(`${path} must be an array.`);
|
|
603
|
+
}
|
|
604
|
+
const identities = value.map((message, index) => validateMessage(message, `${path}[${index}]`));
|
|
605
|
+
const messageIds = new Set<string>();
|
|
606
|
+
for (const identity of identities) {
|
|
607
|
+
if (messageIds.has(identity.messageId)) {
|
|
608
|
+
throw new FlexHarnessStoreFormatError(`${path} contains duplicate message "${identity.messageId}".`);
|
|
540
609
|
}
|
|
541
|
-
|
|
542
|
-
|
|
610
|
+
messageIds.add(identity.messageId);
|
|
611
|
+
if (expectedSessionId !== undefined && identity.sessionId !== expectedSessionId) {
|
|
612
|
+
throw new FlexHarnessStoreFormatError(`${path} contains a message for another session.`);
|
|
543
613
|
}
|
|
544
614
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
615
|
+
return identities;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function validatePermissionKeys(value: unknown, path: string): void {
|
|
619
|
+
const keys = requireStringArray(value, path);
|
|
620
|
+
if (keys.some((key) => key.length === 0)) {
|
|
621
|
+
throw new FlexHarnessStoreFormatError(`${path} contains an empty key.`);
|
|
551
622
|
}
|
|
552
|
-
if (new Set(
|
|
553
|
-
throw new FlexHarnessStoreFormatError(`${path}
|
|
623
|
+
if (new Set(keys).size !== keys.length) {
|
|
624
|
+
throw new FlexHarnessStoreFormatError(`${path} contains duplicate keys.`);
|
|
554
625
|
}
|
|
555
|
-
return sessionId;
|
|
556
626
|
}
|
|
557
627
|
|
|
558
|
-
|
|
628
|
+
function validateSnapshotHeader(
|
|
629
|
+
value: unknown,
|
|
630
|
+
allowedKeys: readonly string[],
|
|
631
|
+
): Record<string, unknown> {
|
|
559
632
|
assertJsonSerializable(value, '$snapshot');
|
|
560
633
|
const snapshot = requireRecord(value, '$snapshot');
|
|
634
|
+
requireOnlyKeys(snapshot, allowedKeys, '$snapshot');
|
|
561
635
|
if (snapshot.schemaVersion !== 1) {
|
|
562
636
|
throw new FlexHarnessStoreFormatError('Snapshot schemaVersion must be 1.');
|
|
563
637
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
638
|
+
requireNonNegativeInteger(snapshot.revision, 'Snapshot revision');
|
|
639
|
+
return snapshot;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexScopeSnapshot {
|
|
643
|
+
const snapshot = validateSnapshotHeader(value, [
|
|
644
|
+
'schemaVersion',
|
|
645
|
+
'revision',
|
|
646
|
+
'sessions',
|
|
647
|
+
'tombstones',
|
|
648
|
+
]);
|
|
567
649
|
if (!Array.isArray(snapshot.sessions)) {
|
|
568
650
|
throw new FlexHarnessStoreFormatError('Snapshot sessions must be an array.');
|
|
569
651
|
}
|
|
570
652
|
const sessionIds = new Set<string>();
|
|
571
653
|
for (let index = 0; index < snapshot.sessions.length; index++) {
|
|
572
|
-
const sessionId =
|
|
654
|
+
const sessionId = validateSession(snapshot.sessions[index], `$snapshot.sessions[${index}]`);
|
|
573
655
|
if (sessionIds.has(sessionId)) {
|
|
574
656
|
throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate session "${sessionId}".`);
|
|
575
657
|
}
|
|
576
658
|
sessionIds.add(sessionId);
|
|
577
659
|
}
|
|
660
|
+
if (!Array.isArray(snapshot.tombstones)) {
|
|
661
|
+
throw new FlexHarnessStoreFormatError('Snapshot tombstones must be an array.');
|
|
662
|
+
}
|
|
663
|
+
const tombstoneIds = new Set<string>();
|
|
664
|
+
for (let index = 0; index < snapshot.tombstones.length; index++) {
|
|
665
|
+
const path = `$snapshot.tombstones[${index}]`;
|
|
666
|
+
const tombstone = requireRecord(snapshot.tombstones[index], path);
|
|
667
|
+
requireOnlyKeys(tombstone, ['sessionId', 'deletedAt'], path);
|
|
668
|
+
const sessionId = requireString(tombstone.sessionId, `${path}.sessionId`);
|
|
669
|
+
requireString(tombstone.deletedAt, `${path}.deletedAt`);
|
|
670
|
+
if (tombstoneIds.has(sessionId)) {
|
|
671
|
+
throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate tombstone "${sessionId}".`);
|
|
672
|
+
}
|
|
673
|
+
if (sessionIds.has(sessionId)) {
|
|
674
|
+
throw new FlexHarnessStoreFormatError(
|
|
675
|
+
`Snapshot session "${sessionId}" cannot also have a tombstone.`,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
tombstoneIds.add(sessionId);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function assertFlexProjectionSnapshot(
|
|
683
|
+
value: unknown,
|
|
684
|
+
): asserts value is IFlexProjectionSnapshot {
|
|
685
|
+
const snapshot = validateSnapshotHeader(value, [
|
|
686
|
+
'schemaVersion',
|
|
687
|
+
'revision',
|
|
688
|
+
'messages',
|
|
689
|
+
'stagedTerminals',
|
|
690
|
+
]);
|
|
691
|
+
const visibleMessages = validateMessages(snapshot.messages, '$snapshot.messages');
|
|
692
|
+
let projectionSessionId = visibleMessages[0]?.sessionId;
|
|
693
|
+
if (visibleMessages.some((message) => message.sessionId !== projectionSessionId)) {
|
|
694
|
+
throw new FlexHarnessStoreFormatError('Snapshot messages must belong to one session.');
|
|
695
|
+
}
|
|
696
|
+
if (!Array.isArray(snapshot.stagedTerminals)) {
|
|
697
|
+
throw new FlexHarnessStoreFormatError('Snapshot stagedTerminals must be an array.');
|
|
698
|
+
}
|
|
699
|
+
const runIds = new Set<string>();
|
|
700
|
+
const stagedMessageIds = new Set<string>();
|
|
701
|
+
for (let index = 0; index < snapshot.stagedTerminals.length; index++) {
|
|
702
|
+
const path = `$snapshot.stagedTerminals[${index}]`;
|
|
703
|
+
const terminal = requireRecord(snapshot.stagedTerminals[index], path);
|
|
704
|
+
requireOnlyKeys(
|
|
705
|
+
terminal,
|
|
706
|
+
[
|
|
707
|
+
'runId',
|
|
708
|
+
'status',
|
|
709
|
+
'userMessage',
|
|
710
|
+
'assistantMessage',
|
|
711
|
+
'model',
|
|
712
|
+
'usage',
|
|
713
|
+
'finishReason',
|
|
714
|
+
'steps',
|
|
715
|
+
],
|
|
716
|
+
path,
|
|
717
|
+
);
|
|
718
|
+
const runId = requireString(terminal.runId, `${path}.runId`);
|
|
719
|
+
if (runIds.has(runId)) {
|
|
720
|
+
throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate staged run "${runId}".`);
|
|
721
|
+
}
|
|
722
|
+
runIds.add(runId);
|
|
723
|
+
if (!['completed', 'failed', 'cancelled'].includes(String(terminal.status))) {
|
|
724
|
+
throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
|
|
725
|
+
}
|
|
726
|
+
const user = validateMessage(terminal.userMessage, `${path}.userMessage`);
|
|
727
|
+
const assistant = validateMessage(terminal.assistantMessage, `${path}.assistantMessage`);
|
|
728
|
+
if (user.role !== 'user' || assistant.role !== 'assistant') {
|
|
729
|
+
throw new FlexHarnessStoreFormatError(`${path} has invalid terminal message roles.`);
|
|
730
|
+
}
|
|
731
|
+
if (user.runId !== runId || assistant.runId !== runId || user.sessionId !== assistant.sessionId) {
|
|
732
|
+
throw new FlexHarnessStoreFormatError(`${path} has uncorrelated terminal messages.`);
|
|
733
|
+
}
|
|
734
|
+
if (assistant.status !== terminal.status) {
|
|
735
|
+
throw new FlexHarnessStoreFormatError(`${path}.status does not match its assistant message.`);
|
|
736
|
+
}
|
|
737
|
+
projectionSessionId ??= user.sessionId;
|
|
738
|
+
if (user.sessionId !== projectionSessionId) {
|
|
739
|
+
throw new FlexHarnessStoreFormatError(`${path} belongs to another session.`);
|
|
740
|
+
}
|
|
741
|
+
for (const messageId of [user.messageId, assistant.messageId]) {
|
|
742
|
+
if (stagedMessageIds.has(messageId)) {
|
|
743
|
+
throw new FlexHarnessStoreFormatError(
|
|
744
|
+
`Snapshot staged terminals contain duplicate message "${messageId}".`,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
stagedMessageIds.add(messageId);
|
|
748
|
+
}
|
|
749
|
+
if (terminal.model !== undefined) validateModel(terminal.model, `${path}.model`);
|
|
750
|
+
if (terminal.usage !== undefined) validateUsage(terminal.usage, `${path}.usage`);
|
|
751
|
+
requireOptionalString(terminal.finishReason, `${path}.finishReason`);
|
|
752
|
+
if (terminal.steps !== undefined) requireNonNegativeInteger(terminal.steps, `${path}.steps`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function assertFlexPermissionSnapshot(
|
|
757
|
+
value: unknown,
|
|
758
|
+
): asserts value is IFlexPermissionSnapshot {
|
|
759
|
+
const snapshot = validateSnapshotHeader(value, [
|
|
760
|
+
'schemaVersion',
|
|
761
|
+
'revision',
|
|
762
|
+
'rememberedPermissionKeys',
|
|
763
|
+
]);
|
|
764
|
+
validatePermissionKeys(snapshot.rememberedPermissionKeys, '$snapshot.rememberedPermissionKeys');
|
|
578
765
|
}
|
|
579
766
|
|
|
580
767
|
export function cloneSerializable<T>(value: T): T {
|
package/ts/utils.prompt.ts
CHANGED
|
@@ -7,11 +7,13 @@ import type {
|
|
|
7
7
|
TFlexPrompt,
|
|
8
8
|
TFlexPromptPart,
|
|
9
9
|
} from './interfaces.js';
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
assertJsonSerializable,
|
|
12
|
+
cloneSerializable,
|
|
13
|
+
serializeAgentMessages,
|
|
14
|
+
} from './utils.json.js';
|
|
12
15
|
|
|
13
16
|
export interface INormalizedFlexPrompt {
|
|
14
|
-
agentPrompt: TFlexAgentPrompt;
|
|
15
17
|
modelMessage: TFlexAgentModelMessage;
|
|
16
18
|
parts: TFlexPromptPart[];
|
|
17
19
|
}
|
|
@@ -99,27 +101,6 @@ function rejectUnknownKeys(
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
function toAgentParts(parts: TFlexPromptPart[]): TFlexAgentPrompt {
|
|
103
|
-
return parts.map((part) => {
|
|
104
|
-
if (part.type === 'text') {
|
|
105
|
-
return { type: 'text' as const, text: part.text };
|
|
106
|
-
}
|
|
107
|
-
if (part.type === 'image') {
|
|
108
|
-
return {
|
|
109
|
-
type: 'image' as const,
|
|
110
|
-
image: toAgentData(part.data),
|
|
111
|
-
...(part.mediaType ? { mediaType: part.mediaType } : {}),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
type: 'file' as const,
|
|
116
|
-
data: toAgentData(part.data),
|
|
117
|
-
mediaType: part.mediaType,
|
|
118
|
-
...(part.name ? { filename: part.name } : {}),
|
|
119
|
-
};
|
|
120
|
-
}) as TFlexAgentPrompt;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
104
|
function toStoredModelParts(parts: TFlexPromptPart[]): TFlexAgentPrompt {
|
|
124
105
|
return parts.map((part) => {
|
|
125
106
|
if (part.type === 'text') {
|
|
@@ -154,7 +135,6 @@ export function normalizeFlexPrompt(prompt: TFlexPrompt): INormalizedFlexPrompt
|
|
|
154
135
|
throw new FlexHarnessValidationError('prompt must not be empty.');
|
|
155
136
|
}
|
|
156
137
|
return {
|
|
157
|
-
agentPrompt: prompt,
|
|
158
138
|
modelMessage: { role: 'user', content: prompt },
|
|
159
139
|
parts: [{ type: 'text', text: prompt }],
|
|
160
140
|
};
|
|
@@ -164,7 +144,6 @@ export function normalizeFlexPrompt(prompt: TFlexPrompt): INormalizedFlexPrompt
|
|
|
164
144
|
}
|
|
165
145
|
const parts = prompt.map(validatePart);
|
|
166
146
|
return {
|
|
167
|
-
agentPrompt: toAgentParts(parts),
|
|
168
147
|
modelMessage: {
|
|
169
148
|
role: 'user',
|
|
170
149
|
content: toStoredModelParts(parts),
|
|
@@ -176,7 +155,7 @@ export function normalizeFlexPrompt(prompt: TFlexPrompt): INormalizedFlexPrompt
|
|
|
176
155
|
export function hydrateAgentMessages(
|
|
177
156
|
messages: TFlexAgentModelMessage[],
|
|
178
157
|
): TFlexAgentModelMessage[] {
|
|
179
|
-
const hydrated =
|
|
158
|
+
const hydrated = serializeAgentMessages(messages);
|
|
180
159
|
for (const message of hydrated) {
|
|
181
160
|
if (!Array.isArray(message.content)) {
|
|
182
161
|
continue;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './v3_legacyflexharness.js';
|