@fevex/core 0.1.0-alpha.3 → 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.
Files changed (41) hide show
  1. package/README.md +183 -2
  2. package/dist/agents/index.d.mts +1 -1
  3. package/dist/artifacts/index.d.mts +2 -0
  4. package/dist/artifacts/index.mjs +2 -0
  5. package/dist/artifacts-gUzemET7.mjs +440 -0
  6. package/dist/channels/index.d.mts +2 -2
  7. package/dist/channels/index.mjs +70 -28
  8. package/dist/evals/index.d.mts +4 -4
  9. package/dist/{fevex-Ddyi6z6L.d.mts → fevex-B5NxFKcV.d.mts} +41 -11
  10. package/dist/http/index.d.mts +65 -5
  11. package/dist/http/index.mjs +236 -16
  12. package/dist/{index-Bgcxgy66.d.mts → index--_LdSEuF.d.mts} +31 -10
  13. package/dist/{index-CrOId2px.d.mts → index-BFnWUu7j.d.mts} +1 -1
  14. package/dist/{index-CTIwrVf9.d.mts → index-BPwkgdo3.d.mts} +1 -1
  15. package/dist/{index-DviHtsLo.d.mts → index-Br-zPxZH.d.mts} +2 -2
  16. package/dist/{index-Dx_LotWZ.d.mts → index-CGmHXV31.d.mts} +38 -3
  17. package/dist/{index-CqOjvtQ9.d.mts → index-CKQCNvJz.d.mts} +4 -2
  18. package/dist/{index-dkFyTNh4.d.mts → index-Cb3AB98n.d.mts} +2 -2
  19. package/dist/{index-B4PIkqOU.d.mts → index-D6lnI92z.d.mts} +3 -3
  20. package/dist/{index-Bg9rr8Qe.d.mts → index-Dsulk-n6.d.mts} +3 -3
  21. package/dist/{index-DeXUtyd9.d.mts → index-aayvtf0J.d.mts} +2 -2
  22. package/dist/index.d.mts +13 -13
  23. package/dist/index.mjs +464 -103
  24. package/dist/knowledge/index.d.mts +1 -1
  25. package/dist/model-stream-JQdHDDcj.mjs +131 -0
  26. package/dist/models/index.d.mts +1 -1
  27. package/dist/observability/index.d.mts +1 -1
  28. package/dist/policies/index.d.mts +1 -1
  29. package/dist/{model-stream-u2xMsEaf.mjs → run-support-CmZ6Xo6H.mjs} +41 -130
  30. package/dist/runtime/index.d.mts +2 -2
  31. package/dist/runtime/index.mjs +43 -1
  32. package/dist/sandbox/index.d.mts +1 -1
  33. package/dist/sandbox/index.mjs +42 -17
  34. package/dist/teams/index.d.mts +1 -1
  35. package/dist/testing/index.d.mts +13 -7
  36. package/dist/testing/index.mjs +98 -2
  37. package/dist/tools/index.d.mts +2 -2
  38. package/dist/types-BDVUoAU6.d.mts +559 -0
  39. package/dist/workflows/index.d.mts +1 -1
  40. package/package.json +6 -2
  41. 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,5 +1122,23 @@ 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. This bounds event counts, not the byte size of an individual event;
963
- per-event payload limits and data retention remain host responsibilities.
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.
1129
+
1130
+ ### Run model identity
1131
+
1132
+ Agent runs retain the effective `modelName` and a `modelMetadata` snapshot when
1133
+ the gateway supplies metadata. Traces and cost calculators use this identity,
1134
+ including per-request model overrides, after terminal checkpoint cleanup and
1135
+ across pause/resume. These fields are optional for older stored runs; resuming
1136
+ an older run fills them from its checkpoint's resolved model.
1137
+
1138
+ ### Local sandbox limits
1139
+
1140
+ The local development sandbox sends `SIGTERM` on timeout or cancellation, then
1141
+ `SIGKILL` after 100 ms if the process has not closed. Cancellation rejects with
1142
+ the caller's reason after process cleanup. Output limits apply separately to
1143
+ stdout and stderr in UTF-8 bytes, omitting an incomplete trailing character.
1144
+ This subprocess control does not provide operating-system isolation.
@@ -1,2 +1,2 @@
1
- import { n as AgentLimits, r as defineAgent, t as AgentDefinition } from "../index-Dx_LotWZ.mjs";
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 ChannelEvent, c as ChannelMessage, d as ChannelRunOptions, f as ChannelRunResult, i as ChannelError, l as ChannelOutput, n as ChannelAdapter, o as ChannelEventType, r as ChannelContext, s as ChannelFailurePhase, t as handleChannelInput, u as ChannelRunEvent } from "../index-Bgcxgy66.mjs";
2
- export { ChannelAdapter, ChannelContext, ChannelError, ChannelEvent, ChannelEventType, ChannelFailurePhase, ChannelMessage, ChannelOutput, ChannelRunEvent, ChannelRunOptions, ChannelRunResult, handleChannelInput };
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
+ export { ChannelAdapter, ChannelContext, ChannelDeliveryOptions, ChannelElicitationContext, ChannelError, ChannelEvent, ChannelEventType, ChannelFailurePhase, ChannelMessage, ChannelOutput, ChannelRunEvent, ChannelRunOptions, ChannelRunResult, deliverChannelRun, handleChannelInput };