@fevex/core 0.1.0-alpha.4 → 0.1.0-alpha.5
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/README.md +167 -2
- package/dist/agents/index.d.mts +1 -1
- package/dist/artifacts/index.d.mts +2 -0
- package/dist/artifacts/index.mjs +2 -0
- package/dist/artifacts-gUzemET7.mjs +440 -0
- package/dist/channels/index.d.mts +1 -1
- package/dist/evals/index.d.mts +4 -4
- package/dist/{fevex-BoPbZ-Tw.d.mts → fevex-B5NxFKcV.d.mts} +41 -11
- package/dist/http/index.d.mts +65 -5
- package/dist/http/index.mjs +236 -16
- package/dist/{index-DyyNLuwO.d.mts → index--_LdSEuF.d.mts} +3 -3
- package/dist/{index-CrOId2px.d.mts → index-BFnWUu7j.d.mts} +1 -1
- package/dist/{index-CTIwrVf9.d.mts → index-BPwkgdo3.d.mts} +1 -1
- package/dist/{index-CknU-glw.d.mts → index-Br-zPxZH.d.mts} +2 -2
- package/dist/{index-CaE864Kz.d.mts → index-CGmHXV31.d.mts} +35 -3
- package/dist/{index-CqOjvtQ9.d.mts → index-CKQCNvJz.d.mts} +4 -2
- package/dist/{index-dkFyTNh4.d.mts → index-Cb3AB98n.d.mts} +2 -2
- package/dist/{index-voIZhQXB.d.mts → index-D6lnI92z.d.mts} +3 -3
- package/dist/{index-C-IFfCNw.d.mts → index-Dsulk-n6.d.mts} +3 -3
- package/dist/{index-hhbgvfIt.d.mts → index-aayvtf0J.d.mts} +2 -2
- package/dist/index.d.mts +13 -13
- package/dist/index.mjs +444 -95
- package/dist/knowledge/index.d.mts +1 -1
- package/dist/model-stream-JQdHDDcj.mjs +131 -0
- package/dist/models/index.d.mts +1 -1
- package/dist/observability/index.d.mts +1 -1
- package/dist/policies/index.d.mts +1 -1
- package/dist/{model-stream-u2xMsEaf.mjs → run-support-CmZ6Xo6H.mjs} +41 -130
- package/dist/runtime/index.d.mts +2 -2
- package/dist/runtime/index.mjs +43 -1
- package/dist/sandbox/index.d.mts +1 -1
- package/dist/sandbox/index.mjs +1 -1
- package/dist/teams/index.d.mts +1 -1
- package/dist/testing/index.d.mts +8 -7
- package/dist/testing/index.mjs +94 -2
- package/dist/tools/index.d.mts +2 -2
- package/dist/types-BDVUoAU6.d.mts +559 -0
- package/dist/workflows/index.d.mts +1 -1
- package/package.json +6 -2
- package/dist/types-CuoRx12Q.d.mts +0 -333
package/README.md
CHANGED
|
@@ -568,6 +568,31 @@ Hosts pass authenticated `ExecutionContext` separately to `handler(request,
|
|
|
568
568
|
SSE response never cancels its run. Cancellation is explicit through
|
|
569
569
|
`client.cancelRun(run.id)`.
|
|
570
570
|
|
|
571
|
+
Artifacts use the same handler:
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
const handler = createFevexHttpHandler({
|
|
575
|
+
fevex: app,
|
|
576
|
+
artifacts: artifactStore, // FileSystemArtifactStore or S3ArtifactStore
|
|
577
|
+
artifactCatalog: artifactCatalog, // optional; enables listing by run
|
|
578
|
+
authorizeArtifact(access) {
|
|
579
|
+
return access.context?.actor?.id === 'alice';
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Routes:
|
|
585
|
+
|
|
586
|
+
- `GET /v1/artifacts/:id` streams the bytes with `content-type`,
|
|
587
|
+
`content-length`, `content-disposition`, `etag` and artifact headers.
|
|
588
|
+
- `HEAD /v1/artifacts/:id` returns the same metadata without a body.
|
|
589
|
+
- `DELETE /v1/artifacts/:id` deletes bytes and catalog metadata.
|
|
590
|
+
- `GET /v1/runs/:runId/artifacts?limit=100` lists descriptors for a run.
|
|
591
|
+
|
|
592
|
+
The client exposes `getArtifact`, `listArtifacts` and `deleteArtifact`. Keep the
|
|
593
|
+
bucket private; the handler authorizes the request and streams the bytes, or a
|
|
594
|
+
host can use the descriptor to mint a short-lived signed URL.
|
|
595
|
+
|
|
571
596
|
## Approvals and durable runs
|
|
572
597
|
|
|
573
598
|
Tools can declare risk, approval, idempotency, retries and credential names.
|
|
@@ -652,6 +677,28 @@ underlying work. External systems must enforce the supplied idempotency key to
|
|
|
652
677
|
make keyed retries safe. Repeated tool call IDs are rejected within a run before
|
|
653
678
|
Fevex repeats an effect.
|
|
654
679
|
|
|
680
|
+
### Deadlines, admission and retention
|
|
681
|
+
|
|
682
|
+
Set `deadlineMs` on a run request to cap wall-clock time. An expired deadline
|
|
683
|
+
cancels the run with `run.cancelled` reason `timeout`; resuming or recovering
|
|
684
|
+
after the absolute deadline persists that cancellation instead of continuing.
|
|
685
|
+
|
|
686
|
+
`createFevex({ admission })` limits concurrent top-level runs globally or per
|
|
687
|
+
tenant. The default tenant key is a non-empty `context.attributes.tenantId`;
|
|
688
|
+
`tenantKey` and `authorize` support host-specific quotas. Counts are local to
|
|
689
|
+
one runtime process.
|
|
690
|
+
|
|
691
|
+
`createFevex({ limits: { maxEventsPerRun } })` caps non-control events per run.
|
|
692
|
+
Pause, resume, approval, elicitation, wait and terminal events are always
|
|
693
|
+
persisted so a run can always reach a durable state; use retention for a total
|
|
694
|
+
storage cap.
|
|
695
|
+
|
|
696
|
+
Terminal runs can be purged with `app.purgeRuns({ before, statuses, limit })`.
|
|
697
|
+
SQLite and PostgreSQL implement the retention capability; custom stores can add
|
|
698
|
+
`purgeRuns` to opt in. The API deletes only terminal runs and sessions left
|
|
699
|
+
without runs; active work is untouched. Artifact retention and per-event payload
|
|
700
|
+
byte limits remain host policies.
|
|
701
|
+
|
|
655
702
|
## Observability
|
|
656
703
|
|
|
657
704
|
Add trace exporters to the application configuration; agent definitions stay
|
|
@@ -871,12 +918,128 @@ Context providers are lazy per run. Memory is a replaceable store; the built-in
|
|
|
871
918
|
store is in-memory substring search, not embeddings, vector search or durable
|
|
872
919
|
storage.
|
|
873
920
|
|
|
921
|
+
## Artifacts
|
|
922
|
+
|
|
923
|
+
`@fevex/core/artifacts` stores large or binary outputs as references. Runs,
|
|
924
|
+
events and prompts carry an `ArtifactDescriptor`; bytes stay in an
|
|
925
|
+
`ArtifactStore`.
|
|
926
|
+
|
|
927
|
+
```ts
|
|
928
|
+
import {
|
|
929
|
+
FileSystemArtifactStore,
|
|
930
|
+
InMemoryArtifactStore,
|
|
931
|
+
S3ArtifactStore,
|
|
932
|
+
type S3ObjectClient,
|
|
933
|
+
} from '@fevex/core/artifacts';
|
|
934
|
+
|
|
935
|
+
const artifacts = new FileSystemArtifactStore({
|
|
936
|
+
rootDir: '.fevex/artifacts',
|
|
937
|
+
maxBytes: 10 * 1024 * 1024,
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
const descriptor = await artifacts.put(
|
|
941
|
+
{
|
|
942
|
+
filename: 'report.pdf',
|
|
943
|
+
mimeType: 'application/pdf',
|
|
944
|
+
bytes,
|
|
945
|
+
},
|
|
946
|
+
{ tenantId: 'acme', runId: 'run-1' },
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
const stored = await artifacts.get(descriptor.artifactId, { tenantId: 'acme' });
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
Use `InMemoryArtifactStore` for tests. For S3, R2, MinIO or another
|
|
953
|
+
S3-compatible service, wrap its SDK with the small `S3ObjectClient` interface;
|
|
954
|
+
Fevex does not ship an object-storage SDK:
|
|
955
|
+
|
|
956
|
+
```ts
|
|
957
|
+
const client: S3ObjectClient = {
|
|
958
|
+
async putObject({ key, body, contentType }) {
|
|
959
|
+
await s3.send(new PutObjectCommand({
|
|
960
|
+
Bucket,
|
|
961
|
+
Key: key,
|
|
962
|
+
Body: body,
|
|
963
|
+
ContentType: contentType,
|
|
964
|
+
}));
|
|
965
|
+
},
|
|
966
|
+
async getObject({ key }) {
|
|
967
|
+
try {
|
|
968
|
+
const result = await s3.send(new GetObjectCommand({ Bucket, Key: key }));
|
|
969
|
+
return new Uint8Array(await result.Body!.transformToByteArray());
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if ((error as { name?: string }).name === 'NoSuchKey') return null;
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
},
|
|
975
|
+
async deleteObject({ key }) {
|
|
976
|
+
await s3.send(new DeleteObjectCommand({ Bucket, Key: key }));
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
const artifacts = new S3ArtifactStore({ client, prefix: 'fevex' });
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
The bucket should stay private. The host authorizes the actor and serves a
|
|
984
|
+
short-lived signed URL or streams the bytes; do not expose bucket URLs to the
|
|
985
|
+
model. This subpath is local-first: use the filesystem store for development and
|
|
986
|
+
single-node deployments, and the S3-compatible store for multi-instance
|
|
987
|
+
production.
|
|
988
|
+
|
|
989
|
+
For a queryable metadata index, compose the byte store with a catalog:
|
|
990
|
+
|
|
991
|
+
```ts
|
|
992
|
+
import { CatalogedArtifactStore, FileSystemArtifactStore } from '@fevex/core/artifacts';
|
|
993
|
+
import { createSQLiteArtifactCatalog } from '@fevex/sqlite';
|
|
994
|
+
|
|
995
|
+
const catalog = createSQLiteArtifactCatalog({ filename: '.fevex/artifacts.db' });
|
|
996
|
+
const artifacts = new CatalogedArtifactStore({
|
|
997
|
+
store: new FileSystemArtifactStore({ rootDir: '.fevex/artifacts' }),
|
|
998
|
+
catalog,
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
const records = await artifacts.list({ runId: 'run-1' }, { tenantId: 'acme' });
|
|
1002
|
+
await artifacts.deleteByRun('run-1', { tenantId: 'acme' });
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
`@fevex/postgres` exposes the same catalog contract with
|
|
1006
|
+
`createPostgresArtifactCatalog`. The catalog stores only metadata; bytes stay in
|
|
1007
|
+
the filesystem or object storage.
|
|
1008
|
+
|
|
1009
|
+
Pass the store to `createFevex` to expose it to tools as `context.artifacts`:
|
|
1010
|
+
|
|
1011
|
+
```ts
|
|
1012
|
+
const save = defineTool({
|
|
1013
|
+
name: 'save_report',
|
|
1014
|
+
async execute(_input, context) {
|
|
1015
|
+
return context.artifacts!.put({
|
|
1016
|
+
filename: 'report.txt',
|
|
1017
|
+
mimeType: 'text/plain',
|
|
1018
|
+
bytes: new TextEncoder().encode('report'),
|
|
1019
|
+
});
|
|
1020
|
+
},
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
createFevex({
|
|
1024
|
+
models,
|
|
1025
|
+
agents,
|
|
1026
|
+
tools: [save],
|
|
1027
|
+
artifacts,
|
|
1028
|
+
});
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
The runtime injects `runId`, `sessionId`, tenant and actor into every artifact
|
|
1032
|
+
call. Each successful `put` emits `artifact.created` with the descriptor before
|
|
1033
|
+
`tool.completed`; a tool that writes and then fails still leaves the event and
|
|
1034
|
+
the artifact available.
|
|
1035
|
+
|
|
874
1036
|
## Public Exports
|
|
875
1037
|
|
|
876
1038
|
| Import | Purpose |
|
|
877
1039
|
| --- | --- |
|
|
878
1040
|
| `@fevex/core` | Common runtime, definitions and contracts |
|
|
879
1041
|
| `@fevex/core/agents` | Agent definitions |
|
|
1042
|
+
| `@fevex/core/artifacts` | Artifact descriptors, local stores and S3-compatible adapter |
|
|
880
1043
|
| `@fevex/core/channels` | Channel adapters and message handling |
|
|
881
1044
|
| `@fevex/core/knowledge` | Context providers, skills and memory contracts |
|
|
882
1045
|
| `@fevex/core/models` | Provider-neutral model contracts |
|
|
@@ -959,8 +1122,10 @@ limit for the initial SSE read and every subsequent read. Full pages drain
|
|
|
959
1122
|
without the polling delay; short pages wait for `pollIntervalMs`. Stream demand
|
|
960
1123
|
controls progress, so a slow consumer retains at most one page plus the stream's
|
|
961
1124
|
single queued frame, rather than buffering the run's entire history. Disconnecting
|
|
962
|
-
cancels polling.
|
|
963
|
-
per-event payload limits
|
|
1125
|
+
cancels polling. `maxConnections` caps concurrent SSE streams. This bounds event
|
|
1126
|
+
counts, not the byte size of an individual event; per-event payload limits remain
|
|
1127
|
+
host responsibilities. Use `purgeRuns` for configurable retention of terminal
|
|
1128
|
+
runs.
|
|
964
1129
|
|
|
965
1130
|
### Run model identity
|
|
966
1131
|
|
package/dist/agents/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as AgentLimits, r as defineAgent, t as AgentDefinition } from "../index-
|
|
1
|
+
import { n as AgentLimits, r as defineAgent, t as AgentDefinition } from "../index-CGmHXV31.mjs";
|
|
2
2
|
export { AgentDefinition, AgentLimits, defineAgent };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { A as ArtifactError, C as FileSystemArtifactStore, D as ArtifactCatalog, E as CatalogedArtifactStoreOptions, F as ArtifactStore, I as ArtifactStoreContext, L as ArtifactWrite, M as ArtifactId, N as ArtifactQuery, O as ArtifactContent, P as ArtifactRecord, R as artifactTenantId, S as InMemoryArtifactCatalog, T as CatalogedArtifactStore, _ as S3ArtifactStore, b as InMemoryArtifactStore, j as ArtifactErrorCode, k as ArtifactDescriptor, v as S3ArtifactStoreOptions, w as FileSystemArtifactStoreOptions, x as InMemoryArtifactStoreOptions, y as S3ObjectClient, z as assertArtifactId } from "../types-BDVUoAU6.mjs";
|
|
2
|
+
export { ArtifactCatalog, ArtifactContent, ArtifactDescriptor, ArtifactError, ArtifactErrorCode, ArtifactId, ArtifactQuery, ArtifactRecord, ArtifactStore, ArtifactStoreContext, ArtifactWrite, CatalogedArtifactStore, CatalogedArtifactStoreOptions, FileSystemArtifactStore, FileSystemArtifactStoreOptions, InMemoryArtifactCatalog, InMemoryArtifactStore, InMemoryArtifactStoreOptions, S3ArtifactStore, S3ArtifactStoreOptions, S3ObjectClient, artifactTenantId, assertArtifactId };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as CatalogedArtifactStore, c as assertArtifactId, i as FileSystemArtifactStore, n as InMemoryArtifactStore, o as ArtifactError, r as InMemoryArtifactCatalog, s as artifactTenantId, t as S3ArtifactStore } from "../artifacts-gUzemET7.mjs";
|
|
2
|
+
export { ArtifactError, CatalogedArtifactStore, FileSystemArtifactStore, InMemoryArtifactCatalog, InMemoryArtifactStore, S3ArtifactStore, artifactTenantId, assertArtifactId };
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
//#region src/artifacts/artifacts.ts
|
|
5
|
+
var ArtifactError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ArtifactError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const TENANT_PATTERN = /^[A-Za-z0-9._-]{1,128}$/u;
|
|
14
|
+
const ARTIFACT_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/u;
|
|
15
|
+
/** Resolves and validates the tenant scope used by stores and catalogs. */
|
|
16
|
+
function artifactTenantId(context) {
|
|
17
|
+
const tenantId = context?.tenantId ?? "default";
|
|
18
|
+
if (typeof tenantId !== "string" || !TENANT_PATTERN.test(tenantId) || tenantId === "." || tenantId === "..") throw new ArtifactError("ARTIFACT_INVALID_CONTEXT", "Artifact tenantId must contain only letters, numbers, dot, underscore or hyphen");
|
|
19
|
+
return tenantId;
|
|
20
|
+
}
|
|
21
|
+
/** Validates an artifact id before using it in a query or storage key. */
|
|
22
|
+
function assertArtifactId(artifactId) {
|
|
23
|
+
if (typeof artifactId !== "string" || !ARTIFACT_ID_PATTERN.test(artifactId)) throw new ArtifactError("ARTIFACT_INVALID_ID", "Artifact id is not valid");
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/artifacts/internal.ts
|
|
27
|
+
const DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
|
|
28
|
+
const MIME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/;
|
|
29
|
+
function resolveLimits(options = {}) {
|
|
30
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
31
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact maxBytes must be a non-negative integer");
|
|
32
|
+
if (options.retentionMs !== void 0 && (!Number.isFinite(options.retentionMs) || options.retentionMs < 0)) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact retentionMs must be a non-negative number");
|
|
33
|
+
return {
|
|
34
|
+
...options,
|
|
35
|
+
maxBytes
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function assertArtifactWrite(input, limits) {
|
|
39
|
+
if (!input || typeof input !== "object") throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact write input must be an object");
|
|
40
|
+
if (typeof input.filename !== "string" || input.filename.trim().length === 0 || input.filename.length > 255 || /[\\/\0]/u.test(input.filename)) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact filename must be a non-empty display name without path separators");
|
|
41
|
+
if (typeof input.mimeType !== "string" || !MIME_PATTERN.test(input.mimeType)) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact mimeType must be a valid MIME type");
|
|
42
|
+
if (!(input.bytes instanceof Uint8Array)) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact bytes must be a Uint8Array");
|
|
43
|
+
if (input.bytes.byteLength > (limits.maxBytes ?? DEFAULT_MAX_BYTES)) throw new ArtifactError("ARTIFACT_TOO_LARGE", `Artifact exceeds the ${limits.maxBytes ?? DEFAULT_MAX_BYTES} byte limit`);
|
|
44
|
+
if (limits.allowedMimeTypes !== void 0 && !limits.allowedMimeTypes.some((mimeType) => mimeType.toLowerCase() === input.mimeType.toLowerCase())) throw new ArtifactError("ARTIFACT_MIME_UNSUPPORTED", `Artifact MIME type "${input.mimeType}" is not allowed`);
|
|
45
|
+
if (input.expiresAt !== void 0 && Number.isNaN(Date.parse(input.expiresAt))) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact expiresAt must be a valid ISO date");
|
|
46
|
+
if (input.metadata !== void 0 && !isJsonObject(input.metadata)) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact metadata must be a JSON object");
|
|
47
|
+
}
|
|
48
|
+
function createDescriptor(input, limits) {
|
|
49
|
+
const artifactId = randomUUID();
|
|
50
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
51
|
+
const expiresAt = input.expiresAt ?? (limits.retentionMs === void 0 ? void 0 : new Date(Date.now() + limits.retentionMs).toISOString());
|
|
52
|
+
return {
|
|
53
|
+
artifactId,
|
|
54
|
+
uri: `artifact://${artifactId}`,
|
|
55
|
+
filename: input.filename,
|
|
56
|
+
mimeType: input.mimeType,
|
|
57
|
+
size: input.bytes.byteLength,
|
|
58
|
+
sha256: createHash("sha256").update(input.bytes).digest("hex"),
|
|
59
|
+
createdAt,
|
|
60
|
+
...expiresAt === void 0 ? {} : { expiresAt },
|
|
61
|
+
...input.metadata === void 0 ? {} : { metadata: structuredClone(input.metadata) }
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function assertNotAborted(context) {
|
|
65
|
+
context?.signal?.throwIfAborted();
|
|
66
|
+
}
|
|
67
|
+
function isExpired$1(descriptor) {
|
|
68
|
+
return descriptor.expiresAt !== void 0 && Date.parse(descriptor.expiresAt) <= Date.now();
|
|
69
|
+
}
|
|
70
|
+
function copyBytes(bytes) {
|
|
71
|
+
return new Uint8Array(bytes);
|
|
72
|
+
}
|
|
73
|
+
function parseDescriptor(value) {
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(value);
|
|
77
|
+
} catch {
|
|
78
|
+
throw new ArtifactError("ARTIFACT_CORRUPT", "Artifact metadata is not valid JSON");
|
|
79
|
+
}
|
|
80
|
+
if (!isJsonObject(parsed) || typeof parsed.artifactId !== "string" || typeof parsed.uri !== "string" || typeof parsed.filename !== "string" || typeof parsed.mimeType !== "string" || typeof parsed.size !== "number" || typeof parsed.sha256 !== "string" || typeof parsed.createdAt !== "string") throw new ArtifactError("ARTIFACT_CORRUPT", "Artifact metadata is malformed");
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
|
83
|
+
function isJsonObject(value) {
|
|
84
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/artifacts/cataloged-artifact-store.ts
|
|
88
|
+
/**
|
|
89
|
+
* Composes a byte store with a queryable metadata catalog.
|
|
90
|
+
*
|
|
91
|
+
* `put` writes bytes first and then indexes metadata. If indexing fails, the
|
|
92
|
+
* bytes are deleted so the catalog never points at a half-written artifact.
|
|
93
|
+
*/
|
|
94
|
+
var CatalogedArtifactStore = class {
|
|
95
|
+
#store;
|
|
96
|
+
#catalog;
|
|
97
|
+
constructor(options) {
|
|
98
|
+
this.#store = options.store;
|
|
99
|
+
this.#catalog = options.catalog;
|
|
100
|
+
}
|
|
101
|
+
async put(input, context) {
|
|
102
|
+
const descriptor = await this.#store.put(input, context);
|
|
103
|
+
try {
|
|
104
|
+
await this.#catalog.save(this.#record(descriptor, context), context);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
await this.#store.delete?.(descriptor.artifactId, context);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return descriptor;
|
|
110
|
+
}
|
|
111
|
+
async get(artifactId, context) {
|
|
112
|
+
assertNotAborted(context);
|
|
113
|
+
assertArtifactId(artifactId);
|
|
114
|
+
if (!await this.#catalog.get(artifactId, context)) return null;
|
|
115
|
+
const content = await this.#store.get(artifactId, context);
|
|
116
|
+
if (!content) throw new ArtifactError("ARTIFACT_CORRUPT", `Artifact "${artifactId}" is indexed but its bytes are missing`);
|
|
117
|
+
return content;
|
|
118
|
+
}
|
|
119
|
+
async head(artifactId, context) {
|
|
120
|
+
assertNotAborted(context);
|
|
121
|
+
assertArtifactId(artifactId);
|
|
122
|
+
return (await this.#catalog.get(artifactId, context))?.descriptor ?? null;
|
|
123
|
+
}
|
|
124
|
+
async delete(artifactId, context) {
|
|
125
|
+
assertNotAborted(context);
|
|
126
|
+
assertArtifactId(artifactId);
|
|
127
|
+
const existed = await this.#catalog.get(artifactId, context) !== null;
|
|
128
|
+
await this.#catalog.delete(artifactId, context);
|
|
129
|
+
await this.#store.delete?.(artifactId, context);
|
|
130
|
+
return existed;
|
|
131
|
+
}
|
|
132
|
+
async list(query = {}, context) {
|
|
133
|
+
assertNotAborted(context);
|
|
134
|
+
return this.#catalog.list(query, context);
|
|
135
|
+
}
|
|
136
|
+
async deleteByRun(runId, context) {
|
|
137
|
+
assertNotAborted(context);
|
|
138
|
+
if (typeof runId !== "string" || runId.length === 0) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact runId must be non-empty");
|
|
139
|
+
let deleted = 0;
|
|
140
|
+
for (;;) {
|
|
141
|
+
const records = await this.#catalog.list({
|
|
142
|
+
runId,
|
|
143
|
+
includeExpired: true,
|
|
144
|
+
limit: 100
|
|
145
|
+
}, context);
|
|
146
|
+
if (records.length === 0) break;
|
|
147
|
+
for (const record of records) {
|
|
148
|
+
await this.delete(record.descriptor.artifactId, context);
|
|
149
|
+
deleted += 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return deleted;
|
|
153
|
+
}
|
|
154
|
+
#record(descriptor, context) {
|
|
155
|
+
return {
|
|
156
|
+
descriptor,
|
|
157
|
+
tenantId: artifactTenantId(context),
|
|
158
|
+
...context?.runId === void 0 ? {} : { runId: context.runId },
|
|
159
|
+
...context?.sessionId === void 0 ? {} : { sessionId: context.sessionId },
|
|
160
|
+
...context?.actor?.id === void 0 ? {} : { actorId: context.actor.id }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/artifacts/file-system-artifact-store.ts
|
|
166
|
+
/**
|
|
167
|
+
* Local filesystem artifact store.
|
|
168
|
+
*
|
|
169
|
+
* Layout:
|
|
170
|
+
* <rootDir>/tenants/<tenantId>/artifacts/<artifactId>
|
|
171
|
+
* <rootDir>/tenants/<tenantId>/artifacts/<artifactId>.json
|
|
172
|
+
*
|
|
173
|
+
* Bytes are written before metadata, and both use an atomic rename. This is
|
|
174
|
+
* intended for local development and single-node deployments; use
|
|
175
|
+
* `S3ArtifactStore` for multi-instance production.
|
|
176
|
+
*/
|
|
177
|
+
var FileSystemArtifactStore = class {
|
|
178
|
+
#rootDir;
|
|
179
|
+
#limits;
|
|
180
|
+
constructor(options) {
|
|
181
|
+
if (typeof options.rootDir !== "string" || !options.rootDir.trim()) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact rootDir must be non-empty");
|
|
182
|
+
this.#rootDir = resolve(options.rootDir);
|
|
183
|
+
this.#limits = resolveLimits(options);
|
|
184
|
+
}
|
|
185
|
+
async put(input, context) {
|
|
186
|
+
assertNotAborted(context);
|
|
187
|
+
assertArtifactWrite(input, this.#limits);
|
|
188
|
+
const tenantId = artifactTenantId(context);
|
|
189
|
+
const descriptor = createDescriptor(input, this.#limits);
|
|
190
|
+
const dataPath = this.#dataPath(tenantId, descriptor.artifactId);
|
|
191
|
+
const metadataPath = `${dataPath}.json`;
|
|
192
|
+
await mkdir(join(this.#rootDir, "tenants", tenantId, "artifacts"), { recursive: true });
|
|
193
|
+
await writeAtomic(dataPath, input.bytes);
|
|
194
|
+
try {
|
|
195
|
+
await writeAtomic(metadataPath, JSON.stringify(descriptor));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
await remove(dataPath);
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
return structuredClone(descriptor);
|
|
201
|
+
}
|
|
202
|
+
async get(artifactId, context) {
|
|
203
|
+
assertNotAborted(context);
|
|
204
|
+
assertArtifactId(artifactId);
|
|
205
|
+
const descriptor = await this.#readDescriptor(artifactTenantId(context), artifactId);
|
|
206
|
+
if (!descriptor || isExpired$1(descriptor)) return null;
|
|
207
|
+
try {
|
|
208
|
+
return {
|
|
209
|
+
descriptor,
|
|
210
|
+
bytes: copyBytes(await readFile(this.#dataPath(artifactTenantId(context), artifactId)))
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (isNotFound(error)) return null;
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async head(artifactId, context) {
|
|
218
|
+
assertNotAborted(context);
|
|
219
|
+
assertArtifactId(artifactId);
|
|
220
|
+
const descriptor = await this.#readDescriptor(artifactTenantId(context), artifactId);
|
|
221
|
+
return descriptor && !isExpired$1(descriptor) ? descriptor : null;
|
|
222
|
+
}
|
|
223
|
+
async delete(artifactId, context) {
|
|
224
|
+
assertNotAborted(context);
|
|
225
|
+
assertArtifactId(artifactId);
|
|
226
|
+
const tenantId = artifactTenantId(context);
|
|
227
|
+
const dataPath = this.#dataPath(tenantId, artifactId);
|
|
228
|
+
const existed = await this.#readDescriptor(tenantId, artifactId) !== null;
|
|
229
|
+
await remove(dataPath);
|
|
230
|
+
await remove(`${dataPath}.json`);
|
|
231
|
+
return existed;
|
|
232
|
+
}
|
|
233
|
+
async #readDescriptor(tenantId, artifactId) {
|
|
234
|
+
try {
|
|
235
|
+
return parseDescriptor(await readFile(`${this.#dataPath(tenantId, artifactId)}.json`, "utf8"));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (isNotFound(error)) return null;
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
#dataPath(tenantId, artifactId) {
|
|
242
|
+
return join(this.#rootDir, "tenants", tenantId, "artifacts", artifactId);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
async function writeAtomic(path, data) {
|
|
246
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
247
|
+
try {
|
|
248
|
+
await writeFile(temporary, data);
|
|
249
|
+
await rename(temporary, path);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
await remove(temporary);
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function remove(path) {
|
|
256
|
+
try {
|
|
257
|
+
await unlink(path);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!isNotFound(error)) throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function isNotFound(error) {
|
|
263
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/artifacts/in-memory-artifact-catalog.ts
|
|
267
|
+
/** Volatile artifact metadata catalog for tests and local development. */
|
|
268
|
+
var InMemoryArtifactCatalog = class {
|
|
269
|
+
#records = /* @__PURE__ */ new Map();
|
|
270
|
+
async save(record, context) {
|
|
271
|
+
const tenantId = artifactTenantId(context);
|
|
272
|
+
if (record.tenantId !== tenantId) throw new ArtifactError("ARTIFACT_INVALID_CONTEXT", "Artifact record tenant does not match the store context");
|
|
273
|
+
assertArtifactId(record.descriptor.artifactId);
|
|
274
|
+
this.#records.set(key$1(tenantId, record.descriptor.artifactId), structuredClone(record));
|
|
275
|
+
}
|
|
276
|
+
async get(artifactId, context) {
|
|
277
|
+
assertArtifactId(artifactId);
|
|
278
|
+
const record = this.#records.get(key$1(artifactTenantId(context), artifactId));
|
|
279
|
+
return record && !isExpired(record) ? structuredClone(record) : null;
|
|
280
|
+
}
|
|
281
|
+
async list(query = {}, context) {
|
|
282
|
+
const tenantId = artifactTenantId(context);
|
|
283
|
+
const limit = query.limit ?? 100;
|
|
284
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact query limit must be a positive integer");
|
|
285
|
+
return [...this.#records.values()].filter((record) => record.tenantId === tenantId && (query.includeExpired || !isExpired(record)) && (query.runId === void 0 || record.runId === query.runId) && (query.sessionId === void 0 || record.sessionId === query.sessionId) && (query.actorId === void 0 || record.actorId === query.actorId) && (query.mimeType === void 0 || record.descriptor.mimeType === query.mimeType)).sort((left, right) => right.descriptor.createdAt.localeCompare(left.descriptor.createdAt) || right.descriptor.artifactId.localeCompare(left.descriptor.artifactId)).slice(0, Math.min(limit, 1e3)).map((record) => structuredClone(record));
|
|
286
|
+
}
|
|
287
|
+
async delete(artifactId, context) {
|
|
288
|
+
assertArtifactId(artifactId);
|
|
289
|
+
return this.#records.delete(key$1(artifactTenantId(context), artifactId));
|
|
290
|
+
}
|
|
291
|
+
async deleteByRun(runId, context) {
|
|
292
|
+
if (typeof runId !== "string" || runId.length === 0) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "Artifact runId must be non-empty");
|
|
293
|
+
const tenantId = artifactTenantId(context);
|
|
294
|
+
let deleted = 0;
|
|
295
|
+
for (const [recordKey, record] of this.#records) if (record.tenantId === tenantId && record.runId === runId) {
|
|
296
|
+
this.#records.delete(recordKey);
|
|
297
|
+
deleted += 1;
|
|
298
|
+
}
|
|
299
|
+
return deleted;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
function key$1(tenantId, artifactId) {
|
|
303
|
+
return `${tenantId}:${artifactId}`;
|
|
304
|
+
}
|
|
305
|
+
function isExpired(record) {
|
|
306
|
+
return record.descriptor.expiresAt !== void 0 && Date.parse(record.descriptor.expiresAt) <= Date.now();
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/artifacts/in-memory-artifact-store.ts
|
|
310
|
+
/** Volatile artifact store for tests and local development. */
|
|
311
|
+
var InMemoryArtifactStore = class {
|
|
312
|
+
#records = /* @__PURE__ */ new Map();
|
|
313
|
+
#limits;
|
|
314
|
+
constructor(options = {}) {
|
|
315
|
+
this.#limits = resolveLimits(options);
|
|
316
|
+
}
|
|
317
|
+
async put(input, context) {
|
|
318
|
+
assertNotAborted(context);
|
|
319
|
+
assertArtifactWrite(input, this.#limits);
|
|
320
|
+
const tenantId = artifactTenantId(context);
|
|
321
|
+
const descriptor = createDescriptor(input, this.#limits);
|
|
322
|
+
this.#records.set(key(tenantId, descriptor.artifactId), {
|
|
323
|
+
tenantId,
|
|
324
|
+
descriptor: structuredClone(descriptor),
|
|
325
|
+
bytes: copyBytes(input.bytes)
|
|
326
|
+
});
|
|
327
|
+
return structuredClone(descriptor);
|
|
328
|
+
}
|
|
329
|
+
async get(artifactId, context) {
|
|
330
|
+
assertNotAborted(context);
|
|
331
|
+
assertArtifactId(artifactId);
|
|
332
|
+
const record = this.#records.get(key(artifactTenantId(context), artifactId));
|
|
333
|
+
if (!record || isExpired$1(record.descriptor)) return null;
|
|
334
|
+
return {
|
|
335
|
+
descriptor: structuredClone(record.descriptor),
|
|
336
|
+
bytes: copyBytes(record.bytes)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async head(artifactId, context) {
|
|
340
|
+
assertNotAborted(context);
|
|
341
|
+
assertArtifactId(artifactId);
|
|
342
|
+
const record = this.#records.get(key(artifactTenantId(context), artifactId));
|
|
343
|
+
return record && !isExpired$1(record.descriptor) ? structuredClone(record.descriptor) : null;
|
|
344
|
+
}
|
|
345
|
+
async delete(artifactId, context) {
|
|
346
|
+
assertNotAborted(context);
|
|
347
|
+
assertArtifactId(artifactId);
|
|
348
|
+
return this.#records.delete(key(artifactTenantId(context), artifactId));
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
function key(tenantId, artifactId) {
|
|
352
|
+
return `${tenantId}:${artifactId}`;
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/artifacts/s3-artifact-store.ts
|
|
356
|
+
/**
|
|
357
|
+
* Artifact store for S3-compatible object storage (S3, R2, MinIO, GCS via an
|
|
358
|
+
* S3 gateway, and similar).
|
|
359
|
+
*
|
|
360
|
+
* Bytes are stored at `<prefix>/<tenant>/artifacts/<artifactId>` and metadata
|
|
361
|
+
* in a `.json` sidecar next to them. The bucket should stay private; the host
|
|
362
|
+
* serves downloads through its authenticated API or short-lived signed URLs.
|
|
363
|
+
*/
|
|
364
|
+
var S3ArtifactStore = class {
|
|
365
|
+
#client;
|
|
366
|
+
#prefix;
|
|
367
|
+
#limits;
|
|
368
|
+
constructor(options) {
|
|
369
|
+
if (!options.client) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "S3 artifact client is required");
|
|
370
|
+
this.#client = options.client;
|
|
371
|
+
this.#prefix = normalizePrefix(options.prefix ?? "fevex");
|
|
372
|
+
this.#limits = resolveLimits(options);
|
|
373
|
+
}
|
|
374
|
+
async put(input, context) {
|
|
375
|
+
assertNotAborted(context);
|
|
376
|
+
assertArtifactWrite(input, this.#limits);
|
|
377
|
+
const descriptor = createDescriptor(input, this.#limits);
|
|
378
|
+
const dataKey = this.#key(artifactTenantId(context), descriptor.artifactId);
|
|
379
|
+
const metadataKey = `${dataKey}.json`;
|
|
380
|
+
await this.#client.putObject({
|
|
381
|
+
key: dataKey,
|
|
382
|
+
body: copyBytes(input.bytes),
|
|
383
|
+
contentType: descriptor.mimeType
|
|
384
|
+
});
|
|
385
|
+
try {
|
|
386
|
+
await this.#client.putObject({
|
|
387
|
+
key: metadataKey,
|
|
388
|
+
body: new TextEncoder().encode(JSON.stringify(descriptor)),
|
|
389
|
+
contentType: "application/json"
|
|
390
|
+
});
|
|
391
|
+
} catch (error) {
|
|
392
|
+
await this.#client.deleteObject({ key: dataKey });
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
return structuredClone(descriptor);
|
|
396
|
+
}
|
|
397
|
+
async get(artifactId, context) {
|
|
398
|
+
assertNotAborted(context);
|
|
399
|
+
assertArtifactId(artifactId);
|
|
400
|
+
const descriptor = await this.#readDescriptor(artifactTenantId(context), artifactId);
|
|
401
|
+
if (!descriptor || isExpired$1(descriptor)) return null;
|
|
402
|
+
const bytes = await this.#client.getObject({ key: this.#key(artifactTenantId(context), artifactId) });
|
|
403
|
+
if (!bytes) throw new ArtifactError("ARTIFACT_CORRUPT", `Artifact "${artifactId}" metadata exists without bytes`);
|
|
404
|
+
return {
|
|
405
|
+
descriptor,
|
|
406
|
+
bytes: copyBytes(bytes)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async head(artifactId, context) {
|
|
410
|
+
assertNotAborted(context);
|
|
411
|
+
assertArtifactId(artifactId);
|
|
412
|
+
const descriptor = await this.#readDescriptor(artifactTenantId(context), artifactId);
|
|
413
|
+
return descriptor && !isExpired$1(descriptor) ? descriptor : null;
|
|
414
|
+
}
|
|
415
|
+
async delete(artifactId, context) {
|
|
416
|
+
assertNotAborted(context);
|
|
417
|
+
assertArtifactId(artifactId);
|
|
418
|
+
const tenantId = artifactTenantId(context);
|
|
419
|
+
const dataKey = this.#key(tenantId, artifactId);
|
|
420
|
+
const existed = await this.#readDescriptor(tenantId, artifactId) !== null;
|
|
421
|
+
await this.#client.deleteObject({ key: dataKey });
|
|
422
|
+
await this.#client.deleteObject({ key: `${dataKey}.json` });
|
|
423
|
+
return existed;
|
|
424
|
+
}
|
|
425
|
+
async #readDescriptor(tenantId, artifactId) {
|
|
426
|
+
const metadata = await this.#client.getObject({ key: `${this.#key(tenantId, artifactId)}.json` });
|
|
427
|
+
return metadata ? parseDescriptor(new TextDecoder().decode(metadata)) : null;
|
|
428
|
+
}
|
|
429
|
+
#key(tenantId, artifactId) {
|
|
430
|
+
return `${this.#prefix}/${tenantId}/artifacts/${artifactId}`;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
function normalizePrefix(prefix) {
|
|
434
|
+
if (typeof prefix !== "string") throw new ArtifactError("ARTIFACT_INVALID_INPUT", "S3 artifact prefix must be a string");
|
|
435
|
+
const normalized = prefix.replace(/^\/+|\/+$/gu, "");
|
|
436
|
+
if (normalized.length === 0 || normalized.split("/").some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new ArtifactError("ARTIFACT_INVALID_INPUT", "S3 artifact prefix is not valid");
|
|
437
|
+
return normalized;
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
export { CatalogedArtifactStore as a, assertArtifactId as c, FileSystemArtifactStore as i, InMemoryArtifactStore as n, ArtifactError as o, InMemoryArtifactCatalog as r, artifactTenantId as s, S3ArtifactStore as t };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ChannelDeliveryOptions, c as ChannelEvent, d as ChannelMessage, f as ChannelOutput, h as ChannelRunResult, i as ChannelContext, l as ChannelEventType, m as ChannelRunOptions, n as handleChannelInput, o as ChannelElicitationContext, p as ChannelRunEvent, r as ChannelAdapter, s as ChannelError, t as deliverChannelRun, u as ChannelFailurePhase } from "../index
|
|
1
|
+
import { a as ChannelDeliveryOptions, c as ChannelEvent, d as ChannelMessage, f as ChannelOutput, h as ChannelRunResult, i as ChannelContext, l as ChannelEventType, m as ChannelRunOptions, n as handleChannelInput, o as ChannelElicitationContext, p as ChannelRunEvent, r as ChannelAdapter, s as ChannelError, t as deliverChannelRun, u as ChannelFailurePhase } from "../index--_LdSEuF.mjs";
|
|
2
2
|
export { ChannelAdapter, ChannelContext, ChannelDeliveryOptions, ChannelElicitationContext, ChannelError, ChannelEvent, ChannelEventType, ChannelFailurePhase, ChannelMessage, ChannelOutput, ChannelRunEvent, ChannelRunOptions, ChannelRunResult, deliverChannelRun, handleChannelInput };
|
package/dist/evals/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { l as JsonValue, o as ExecutionContext, t as AgentEvent } from "../types-
|
|
2
|
-
import { r as ModelMetadata, s as ModelUsage } from "../index-
|
|
3
|
-
import { l as TraceCost, t as CostCalculator } from "../index-
|
|
4
|
-
import { t as Fevex } from "../fevex-
|
|
1
|
+
import { l as JsonValue, o as ExecutionContext, t as AgentEvent } from "../types-BDVUoAU6.mjs";
|
|
2
|
+
import { r as ModelMetadata, s as ModelUsage } from "../index-BPwkgdo3.mjs";
|
|
3
|
+
import { l as TraceCost, t as CostCalculator } from "../index-D6lnI92z.mjs";
|
|
4
|
+
import { t as Fevex } from "../fevex-B5NxFKcV.mjs";
|
|
5
5
|
//#region src/evals/canonical-json.d.ts
|
|
6
6
|
/** Serializes JSON-like data with stable object-key ordering. */
|
|
7
7
|
declare function serializeCanonical(value: unknown): string;
|