@modelprofile.com/flexharness-agent 6.0.0 → 6.1.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/dist_ts_agent/classes.sessionengine.d.ts +93 -0
- package/dist_ts_agent/classes.sessionengine.js +2140 -0
- package/dist_ts_agent/index.d.ts +3 -2
- package/dist_ts_agent/index.js +3 -2
- package/dist_ts_agent/persistence.file.d.ts +24 -0
- package/dist_ts_agent/persistence.file.js +199 -0
- package/dist_ts_agent/plugins.d.ts +0 -7
- package/dist_ts_agent/plugins.js +1 -9
- package/dist_ts_agent/plugins.node.d.ts +7 -0
- package/dist_ts_agent/plugins.node.js +9 -0
- package/dist_ts_agent/plugins.portable.d.ts +4 -0
- package/dist_ts_agent/plugins.portable.js +5 -0
- package/dist_ts_agent/runner.d.ts +4 -0
- package/dist_ts_agent/runner.js +34 -0
- package/dist_ts_agent/runtime.interfaces.d.ts +21 -0
- package/dist_ts_agent/runtime.interfaces.js +2 -0
- package/dist_ts_agent/runtime.run.d.ts +3 -0
- package/dist_ts_agent/runtime.run.js +94 -0
- package/dist_ts_agent/smartagent.classes.agent.js +4 -93
- package/dist_ts_agent/smartagent.classes.session.d.ts +5 -88
- package/dist_ts_agent/smartagent.classes.session.js +19 -2134
- package/dist_ts_agent/smartagent.events.js +2 -2
- package/dist_ts_agent/smartagent.persistence.d.ts +2 -22
- package/dist_ts_agent/smartagent.persistence.js +3 -198
- package/package.json +9 -3
- package/readme.md +6 -0
- package/ts_agent/classes.sessionengine.ts +2591 -0
- package/ts_agent/index.ts +2 -2
- package/ts_agent/persistence.file.ts +232 -0
- package/ts_agent/plugins.node.ts +10 -0
- package/ts_agent/plugins.portable.ts +5 -0
- package/ts_agent/plugins.ts +0 -10
- package/ts_agent/readme.md +6 -0
- package/ts_agent/runner.ts +40 -0
- package/ts_agent/runtime.interfaces.ts +21 -0
- package/ts_agent/runtime.run.ts +107 -0
- package/ts_agent/smartagent.classes.agent.ts +4 -101
- package/ts_agent/smartagent.classes.session.ts +24 -2582
- package/ts_agent/smartagent.events.ts +1 -1
- package/ts_agent/smartagent.persistence.ts +2 -224
|
@@ -202,7 +202,7 @@ export type TModelMessageEventIdentityFactory = (
|
|
|
202
202
|
) => string;
|
|
203
203
|
|
|
204
204
|
export const createAgentEventId = (prefix = 'event'): string =>
|
|
205
|
-
`${prefix}_${
|
|
205
|
+
`${prefix}_${globalThis.crypto.randomUUID()}`;
|
|
206
206
|
|
|
207
207
|
export function createAgentEvent<TEvent extends TAgentEvent>(
|
|
208
208
|
event: TAgentEventInput<TEvent>,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as plugins from './plugins.js';
|
|
2
1
|
import type { TAgentEvent } from './smartagent.events.js';
|
|
3
2
|
|
|
4
3
|
const SNAPSHOT_SCHEMA_VERSION = 2 as const;
|
|
@@ -543,9 +542,9 @@ export class AgentEventStoreConflictError extends Error {
|
|
|
543
542
|
}
|
|
544
543
|
}
|
|
545
544
|
|
|
546
|
-
const clone = <TValue>(value: TValue): TValue => structuredClone(value);
|
|
545
|
+
export const clone = <TValue>(value: TValue): TValue => structuredClone(value);
|
|
547
546
|
|
|
548
|
-
const createSnapshot = (
|
|
547
|
+
export const createSnapshot = (
|
|
549
548
|
sessionId: string,
|
|
550
549
|
revision: number,
|
|
551
550
|
events: readonly TAgentEvent[],
|
|
@@ -604,224 +603,3 @@ export class InMemoryAgentEventStore implements IAgentEventStoreV2 {
|
|
|
604
603
|
return archive ? clone(archive) : undefined;
|
|
605
604
|
}
|
|
606
605
|
}
|
|
607
|
-
|
|
608
|
-
export interface IFileAgentEventStoreOptions {
|
|
609
|
-
rootDir: string;
|
|
610
|
-
/** Maximum time spent waiting for another writer. Default: 10 seconds. */
|
|
611
|
-
lockTimeoutMs?: number;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const assertSessionId = (sessionId: string): void => {
|
|
615
|
-
if (!sessionId.trim()) throw new Error('Agent event store sessionId must not be empty.');
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
const decodeSnapshot = (value: unknown, sessionId: string): TAgentEventSnapshot => {
|
|
619
|
-
const snapshot = value as Partial<TAgentEventSnapshot>;
|
|
620
|
-
if (snapshot?.schemaVersion === SNAPSHOT_SCHEMA_VERSION) {
|
|
621
|
-
try {
|
|
622
|
-
return validateAgentEventSnapshotV2(value, sessionId);
|
|
623
|
-
} catch {
|
|
624
|
-
throw new Error(`Invalid agent event snapshot for session "${sessionId}".`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
if (
|
|
628
|
-
!snapshot
|
|
629
|
-
|| snapshot.schemaVersion !== 1
|
|
630
|
-
|| snapshot.sessionId !== sessionId
|
|
631
|
-
|| !Number.isSafeInteger(snapshot.revision)
|
|
632
|
-
|| (snapshot.revision ?? -1) < 1
|
|
633
|
-
|| !Array.isArray(snapshot.events)
|
|
634
|
-
) {
|
|
635
|
-
throw new Error(`Invalid agent event snapshot for session "${sessionId}".`);
|
|
636
|
-
}
|
|
637
|
-
return snapshot as TAgentEventSnapshot;
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
const decodeArchive = (
|
|
641
|
-
value: unknown,
|
|
642
|
-
sessionId: string,
|
|
643
|
-
archiveId: string,
|
|
644
|
-
): TAgentEventArchive => {
|
|
645
|
-
const archive = value as Partial<TAgentEventArchive>;
|
|
646
|
-
if (archive?.schemaVersion === ARCHIVE_SCHEMA_VERSION) {
|
|
647
|
-
try {
|
|
648
|
-
return validateAgentEventArchiveV2(value, sessionId, archiveId);
|
|
649
|
-
} catch {
|
|
650
|
-
throw new Error(`Invalid agent event archive "${archiveId}" for session "${sessionId}".`);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
if (
|
|
654
|
-
!archive
|
|
655
|
-
|| archive.schemaVersion !== 1
|
|
656
|
-
|| archive.sessionId !== sessionId
|
|
657
|
-
|| archive.archiveId !== archiveId
|
|
658
|
-
|| !Array.isArray(archive.events)
|
|
659
|
-
) {
|
|
660
|
-
throw new Error(`Invalid agent event archive "${archiveId}" for session "${sessionId}".`);
|
|
661
|
-
}
|
|
662
|
-
return archive as TAgentEventArchive;
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
const sleep = async (milliseconds: number): Promise<void> => {
|
|
666
|
-
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
export class FileAgentEventStore implements IAgentEventStoreV2 {
|
|
670
|
-
public readonly eventSchemaVersion = 2 as const;
|
|
671
|
-
private readonly rootDir: string;
|
|
672
|
-
private readonly lockTimeoutMs: number;
|
|
673
|
-
|
|
674
|
-
constructor(options: IFileAgentEventStoreOptions) {
|
|
675
|
-
if (!options.rootDir.trim()) throw new Error('FileAgentEventStore rootDir must not be empty.');
|
|
676
|
-
this.rootDir = plugins.path.resolve(options.rootDir);
|
|
677
|
-
this.lockTimeoutMs = options.lockTimeoutMs ?? 10_000;
|
|
678
|
-
if (!Number.isSafeInteger(this.lockTimeoutMs) || this.lockTimeoutMs <= 0) {
|
|
679
|
-
throw new Error('lockTimeoutMs must be a positive integer.');
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
private sessionKey(sessionId: string): string {
|
|
684
|
-
assertSessionId(sessionId);
|
|
685
|
-
return plugins.crypto.createHash('sha256').update(sessionId).digest('hex');
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
private snapshotPath(sessionId: string): string {
|
|
689
|
-
return plugins.path.join(this.rootDir, 'sessions', `${this.sessionKey(sessionId)}.bin`);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
private archivePath(sessionId: string, archiveId: string): string {
|
|
693
|
-
if (!archiveId.trim()) throw new Error('Agent event archiveId must not be empty.');
|
|
694
|
-
const archiveKey = plugins.crypto.createHash('sha256').update(archiveId).digest('hex');
|
|
695
|
-
return plugins.path.join(this.rootDir, 'archives', this.sessionKey(sessionId), `${archiveKey}.bin`);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
private async readValue(filePath: string): Promise<unknown | undefined> {
|
|
699
|
-
try {
|
|
700
|
-
return plugins.v8.deserialize(await plugins.fs.readFile(filePath));
|
|
701
|
-
} catch (error) {
|
|
702
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
|
|
703
|
-
throw error;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
private async syncDirectory(directoryPath: string): Promise<void> {
|
|
708
|
-
const handle = await plugins.fs.open(directoryPath, 'r');
|
|
709
|
-
try {
|
|
710
|
-
await handle.sync();
|
|
711
|
-
} finally {
|
|
712
|
-
await handle.close();
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
private async writeAtomic(filePath: string, value: unknown): Promise<void> {
|
|
717
|
-
const directoryPath = plugins.path.dirname(filePath);
|
|
718
|
-
await plugins.fs.mkdir(directoryPath, { recursive: true });
|
|
719
|
-
const temporaryPath = `${filePath}.${process.pid}.${plugins.crypto.randomUUID()}.tmp`;
|
|
720
|
-
let handle: Awaited<ReturnType<typeof plugins.fs.open>> | undefined;
|
|
721
|
-
try {
|
|
722
|
-
handle = await plugins.fs.open(temporaryPath, 'wx');
|
|
723
|
-
await handle.writeFile(plugins.v8.serialize(value));
|
|
724
|
-
await handle.sync();
|
|
725
|
-
await handle.close();
|
|
726
|
-
handle = undefined;
|
|
727
|
-
await plugins.fs.rename(temporaryPath, filePath);
|
|
728
|
-
await this.syncDirectory(directoryPath);
|
|
729
|
-
} finally {
|
|
730
|
-
if (handle) await handle.close().catch(() => undefined);
|
|
731
|
-
await plugins.fs.rm(temporaryPath, { force: true });
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
private async withSessionLock<TValue>(
|
|
736
|
-
sessionId: string,
|
|
737
|
-
operation: () => Promise<TValue>,
|
|
738
|
-
): Promise<TValue> {
|
|
739
|
-
const lockPath = plugins.path.join(this.rootDir, 'locks', `${this.sessionKey(sessionId)}.lock`);
|
|
740
|
-
const ownerPath = plugins.path.join(lockPath, 'owner');
|
|
741
|
-
const ownerToken = plugins.crypto.randomUUID();
|
|
742
|
-
const ownerRecord = JSON.stringify({ token: ownerToken });
|
|
743
|
-
await plugins.fs.mkdir(plugins.path.dirname(lockPath), { recursive: true });
|
|
744
|
-
const startedAt = Date.now();
|
|
745
|
-
while (true) {
|
|
746
|
-
try {
|
|
747
|
-
await plugins.fs.mkdir(lockPath);
|
|
748
|
-
try {
|
|
749
|
-
await plugins.fs.writeFile(ownerPath, ownerRecord, 'utf8');
|
|
750
|
-
} catch (error) {
|
|
751
|
-
await plugins.fs.rm(lockPath, { recursive: true, force: true });
|
|
752
|
-
throw error;
|
|
753
|
-
}
|
|
754
|
-
break;
|
|
755
|
-
} catch (error) {
|
|
756
|
-
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
757
|
-
if (Date.now() - startedAt >= this.lockTimeoutMs) {
|
|
758
|
-
throw new Error(
|
|
759
|
-
`Timed out waiting for the event-store lock for session "${sessionId}" at "${lockPath}".`,
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
await sleep(20);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
try {
|
|
766
|
-
return await operation();
|
|
767
|
-
} finally {
|
|
768
|
-
let stillOwner = false;
|
|
769
|
-
try {
|
|
770
|
-
const owner = JSON.parse(await plugins.fs.readFile(ownerPath, 'utf8')) as { token?: unknown };
|
|
771
|
-
stillOwner = owner.token === ownerToken;
|
|
772
|
-
} catch {}
|
|
773
|
-
if (stillOwner) await plugins.fs.rm(lockPath, { recursive: true, force: true });
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
public async load(sessionId: string): Promise<TAgentEventSnapshot | undefined> {
|
|
778
|
-
const value = await this.readValue(this.snapshotPath(sessionId));
|
|
779
|
-
return value === undefined ? undefined : decodeSnapshot(value, sessionId);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
public async save(
|
|
783
|
-
sessionId: string,
|
|
784
|
-
events: readonly TAgentEvent[],
|
|
785
|
-
expectedRevision: number,
|
|
786
|
-
): Promise<number> {
|
|
787
|
-
return this.withSessionLock(sessionId, async () => {
|
|
788
|
-
const filePath = this.snapshotPath(sessionId);
|
|
789
|
-
const currentValue = await this.readValue(filePath);
|
|
790
|
-
const actualRevision = currentValue === undefined
|
|
791
|
-
? 0
|
|
792
|
-
: decodeSnapshot(currentValue, sessionId).revision;
|
|
793
|
-
if (actualRevision !== expectedRevision) {
|
|
794
|
-
throw new AgentEventStoreConflictError(sessionId, expectedRevision, actualRevision);
|
|
795
|
-
}
|
|
796
|
-
const revision = expectedRevision + 1;
|
|
797
|
-
await this.writeAtomic(filePath, createSnapshot(sessionId, revision, events));
|
|
798
|
-
return revision;
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
public async archive(archive: IAgentEventArchiveV2): Promise<void> {
|
|
803
|
-
const nextArchive = clone(archive);
|
|
804
|
-
await this.withSessionLock(archive.sessionId, async () => {
|
|
805
|
-
const filePath = this.archivePath(archive.sessionId, archive.archiveId);
|
|
806
|
-
const currentValue = await this.readValue(filePath);
|
|
807
|
-
if (currentValue !== undefined) {
|
|
808
|
-
const current = decodeArchive(currentValue, archive.sessionId, archive.archiveId);
|
|
809
|
-
const currentIds = current.events.map((event) => event.id).join('\n');
|
|
810
|
-
const nextIds = nextArchive.events.map((event) => event.id).join('\n');
|
|
811
|
-
if (currentIds !== nextIds) {
|
|
812
|
-
throw new Error(`Archive "${archive.archiveId}" already exists with different events.`);
|
|
813
|
-
}
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
await this.writeAtomic(filePath, nextArchive);
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
public async loadArchive(
|
|
821
|
-
sessionId: string,
|
|
822
|
-
archiveId: string,
|
|
823
|
-
): Promise<TAgentEventArchive | undefined> {
|
|
824
|
-
const value = await this.readValue(this.archivePath(sessionId, archiveId));
|
|
825
|
-
return value === undefined ? undefined : decodeArchive(value, sessionId, archiveId);
|
|
826
|
-
}
|
|
827
|
-
}
|