@zhivex-ai/core 0.7.0 → 0.8.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/README.md +5 -0
- package/dist/advanced-tool-registry.d.ts +112 -0
- package/dist/advanced-tool-registry.d.ts.map +1 -0
- package/dist/advanced-tool-registry.js +407 -0
- package/dist/advanced-tool-registry.js.map +1 -0
- package/dist/agent-evaluation.d.ts +176 -0
- package/dist/agent-evaluation.d.ts.map +1 -0
- package/dist/agent-evaluation.js +334 -0
- package/dist/agent-evaluation.js.map +1 -0
- package/dist/agent-store.d.ts.map +1 -1
- package/dist/agent-store.js +226 -8
- package/dist/agent-store.js.map +1 -1
- package/dist/agent-trace.d.ts +127 -0
- package/dist/agent-trace.d.ts.map +1 -0
- package/dist/agent-trace.js +331 -0
- package/dist/agent-trace.js.map +1 -0
- package/dist/agent.d.ts +12 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +469 -36
- package/dist/agent.js.map +1 -1
- package/dist/api-stability.d.ts +9 -0
- package/dist/api-stability.d.ts.map +1 -0
- package/dist/api-stability.js +261 -0
- package/dist/api-stability.js.map +1 -0
- package/dist/artifact.d.ts +165 -0
- package/dist/artifact.d.ts.map +1 -0
- package/dist/artifact.js +994 -0
- package/dist/artifact.js.map +1 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/generate-text.d.ts.map +1 -1
- package/dist/generate-text.js +6 -2
- package/dist/generate-text.js.map +1 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/live-agent.d.ts.map +1 -1
- package/dist/live-agent.js +7 -1
- package/dist/live-agent.js.map +1 -1
- package/dist/provider-parity.d.ts +63 -0
- package/dist/provider-parity.d.ts.map +1 -0
- package/dist/provider-parity.js +175 -0
- package/dist/provider-parity.js.map +1 -0
- package/dist/runner.d.ts +111 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +635 -0
- package/dist/runner.js.map +1 -0
- package/dist/safety-policy.d.ts +65 -0
- package/dist/safety-policy.d.ts.map +1 -0
- package/dist/safety-policy.js +308 -0
- package/dist/safety-policy.js.map +1 -0
- package/dist/types.d.ts +119 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/workflow-artifacts.d.ts +28 -0
- package/dist/workflow-artifacts.d.ts.map +1 -0
- package/dist/workflow-artifacts.js +86 -0
- package/dist/workflow-artifacts.js.map +1 -0
- package/dist/workflow-evaluation-diff.d.ts +51 -0
- package/dist/workflow-evaluation-diff.d.ts.map +1 -0
- package/dist/workflow-evaluation-diff.js +141 -0
- package/dist/workflow-evaluation-diff.js.map +1 -0
- package/dist/workflow-evaluation.d.ts +94 -0
- package/dist/workflow-evaluation.d.ts.map +1 -0
- package/dist/workflow-evaluation.js +210 -0
- package/dist/workflow-evaluation.js.map +1 -0
- package/dist/workflow-state-service.d.ts +67 -0
- package/dist/workflow-state-service.d.ts.map +1 -0
- package/dist/workflow-state-service.js +498 -0
- package/dist/workflow-state-service.js.map +1 -0
- package/dist/workflow.d.ts +206 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +727 -0
- package/dist/workflow.js.map +1 -0
- package/package.json +1 -1
package/dist/artifact.js
ADDED
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ConflictError, ValidationError } from "./errors.js";
|
|
5
|
+
const randomId = (prefix) => `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
6
|
+
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
|
|
7
|
+
export const ARTIFACT_SCHEMA_VERSION = 1;
|
|
8
|
+
const artifactKey = (input) => `${input.appName}:${input.userId}:${input.sessionId}:${input.id}`;
|
|
9
|
+
const identifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10
|
+
export const normalizeArtifactRecord = (value) => {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
12
|
+
throw new ValidationError("ArtifactRecord must be an object.");
|
|
13
|
+
}
|
|
14
|
+
const artifact = value;
|
|
15
|
+
if (artifact.schemaVersion !== undefined && artifact.schemaVersion > ARTIFACT_SCHEMA_VERSION) {
|
|
16
|
+
throw new ValidationError(`Unsupported ArtifactRecord schemaVersion ${artifact.schemaVersion}.`);
|
|
17
|
+
}
|
|
18
|
+
if (typeof artifact.id !== "string" ||
|
|
19
|
+
typeof artifact.appName !== "string" ||
|
|
20
|
+
typeof artifact.userId !== "string" ||
|
|
21
|
+
typeof artifact.sessionId !== "string" ||
|
|
22
|
+
typeof artifact.name !== "string" ||
|
|
23
|
+
typeof artifact.contentType !== "string" ||
|
|
24
|
+
typeof artifact.createdAt !== "number" ||
|
|
25
|
+
typeof artifact.updatedAt !== "number" ||
|
|
26
|
+
!("data" in artifact)) {
|
|
27
|
+
throw new ValidationError("ArtifactRecord is missing required fields.");
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
31
|
+
revision: typeof artifact.revision === "number" ? artifact.revision : 1,
|
|
32
|
+
id: artifact.id,
|
|
33
|
+
appName: artifact.appName,
|
|
34
|
+
userId: artifact.userId,
|
|
35
|
+
sessionId: artifact.sessionId,
|
|
36
|
+
workflowRunId: artifact.workflowRunId,
|
|
37
|
+
workflowStepId: artifact.workflowStepId,
|
|
38
|
+
agentRunId: artifact.agentRunId,
|
|
39
|
+
name: artifact.name,
|
|
40
|
+
contentType: artifact.contentType,
|
|
41
|
+
data: cloneJson(artifact.data),
|
|
42
|
+
encoding: artifact.encoding,
|
|
43
|
+
size: artifact.size,
|
|
44
|
+
sha256: artifact.sha256,
|
|
45
|
+
storageMode: artifact.storageMode ?? "json",
|
|
46
|
+
blobPath: artifact.blobPath,
|
|
47
|
+
metadata: artifact.metadata ? cloneJson(artifact.metadata) : undefined,
|
|
48
|
+
createdAt: artifact.createdAt,
|
|
49
|
+
updatedAt: artifact.updatedAt
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export const migrateArtifactRecord = (value, targetVersion = ARTIFACT_SCHEMA_VERSION) => {
|
|
53
|
+
if (targetVersion !== ARTIFACT_SCHEMA_VERSION) {
|
|
54
|
+
throw new ValidationError(`Unsupported ArtifactRecord migration target ${targetVersion}.`);
|
|
55
|
+
}
|
|
56
|
+
return normalizeArtifactRecord(value);
|
|
57
|
+
};
|
|
58
|
+
const cloneArtifact = (artifact) => cloneJson(normalizeArtifactRecord(artifact));
|
|
59
|
+
const bytesFromBinaryInput = (data) => {
|
|
60
|
+
const buffer = typeof data === "string"
|
|
61
|
+
? Buffer.from(data, "utf8")
|
|
62
|
+
: data instanceof Uint8Array
|
|
63
|
+
? Buffer.from(data)
|
|
64
|
+
: Buffer.from(data);
|
|
65
|
+
return new Uint8Array(buffer);
|
|
66
|
+
};
|
|
67
|
+
const sha256Digest = (data) => createHash("sha256").update(data).digest("hex");
|
|
68
|
+
const assertExpectedRevision = (current, expectedRevision, resource) => {
|
|
69
|
+
if (expectedRevision !== undefined && (current?.revision ?? 0) !== expectedRevision) {
|
|
70
|
+
throw new ConflictError(`${resource} revision conflict.`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const sqliteMutationCount = (result) => {
|
|
74
|
+
if (!result || typeof result !== "object") {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const record = result;
|
|
78
|
+
const value = record.changes ?? record.changeset ?? record.rowCount;
|
|
79
|
+
return typeof value === "number" ? value : undefined;
|
|
80
|
+
};
|
|
81
|
+
const validateArtifactMetadata = (input) => {
|
|
82
|
+
if (input.size !== undefined && (!Number.isInteger(input.size) || input.size < 0)) {
|
|
83
|
+
throw new ValidationError('The "size" artifact option must be a non-negative integer.');
|
|
84
|
+
}
|
|
85
|
+
if (input.sha256 !== undefined && !/^[a-f0-9]{64}$/i.test(input.sha256)) {
|
|
86
|
+
throw new ValidationError('The "sha256" artifact option must be a 64-character hexadecimal digest.');
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const validateIdentifier = (value, fieldName) => {
|
|
90
|
+
if (!identifierPattern.test(value)) {
|
|
91
|
+
throw new ValidationError(`The "${fieldName}" option must match the SQL identifier pattern [A-Za-z_][A-Za-z0-9_]*.`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
};
|
|
95
|
+
const getRecordField = (value, candidates) => {
|
|
96
|
+
if (!value || typeof value !== "object") {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const record = value;
|
|
100
|
+
for (const candidate of candidates) {
|
|
101
|
+
if (candidate in record) {
|
|
102
|
+
return record[candidate];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
};
|
|
107
|
+
const parseArtifactJson = (value) => {
|
|
108
|
+
if (!value) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
return normalizeArtifactRecord(JSON.parse(value));
|
|
113
|
+
}
|
|
114
|
+
return normalizeArtifactRecord(value);
|
|
115
|
+
};
|
|
116
|
+
const prepareSqliteStatement = (db, sql) => {
|
|
117
|
+
if (typeof db.prepare === "function") {
|
|
118
|
+
return db.prepare(sql);
|
|
119
|
+
}
|
|
120
|
+
if (typeof db.query === "function") {
|
|
121
|
+
return db.query(sql);
|
|
122
|
+
}
|
|
123
|
+
throw new ValidationError('The "db" option must expose either a "prepare()" or "query()" method.');
|
|
124
|
+
};
|
|
125
|
+
const ensurePostgresTable = (() => {
|
|
126
|
+
const initializedTables = new WeakMap();
|
|
127
|
+
return async (client, tableName, createSql) => {
|
|
128
|
+
let tables = initializedTables.get(client);
|
|
129
|
+
if (!tables) {
|
|
130
|
+
tables = new Map();
|
|
131
|
+
initializedTables.set(client, tables);
|
|
132
|
+
}
|
|
133
|
+
let initialization = tables.get(tableName);
|
|
134
|
+
if (!initialization) {
|
|
135
|
+
initialization = Promise.resolve(client.query(createSql, [])).then(() => undefined);
|
|
136
|
+
tables.set(tableName, initialization);
|
|
137
|
+
}
|
|
138
|
+
await initialization;
|
|
139
|
+
};
|
|
140
|
+
})();
|
|
141
|
+
const createArtifact = (input, existing) => {
|
|
142
|
+
validateArtifactMetadata(input);
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
return {
|
|
145
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
146
|
+
revision: existing ? existing.revision + 1 : 1,
|
|
147
|
+
id: input.id ?? existing?.id ?? randomId("art"),
|
|
148
|
+
appName: input.appName,
|
|
149
|
+
userId: input.userId,
|
|
150
|
+
sessionId: input.sessionId,
|
|
151
|
+
workflowRunId: input.workflowRunId,
|
|
152
|
+
workflowStepId: input.workflowStepId,
|
|
153
|
+
agentRunId: input.agentRunId,
|
|
154
|
+
name: input.name,
|
|
155
|
+
contentType: input.contentType,
|
|
156
|
+
data: cloneJson(input.data),
|
|
157
|
+
encoding: input.encoding,
|
|
158
|
+
size: input.size,
|
|
159
|
+
sha256: input.sha256,
|
|
160
|
+
storageMode: input.storageMode ?? "json",
|
|
161
|
+
blobPath: input.blobPath,
|
|
162
|
+
metadata: input.metadata ? cloneJson(input.metadata) : undefined,
|
|
163
|
+
createdAt: existing?.createdAt ?? now,
|
|
164
|
+
updatedAt: now
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
const matchesListInput = (artifact, input) => artifact.appName === input.appName &&
|
|
168
|
+
artifact.userId === input.userId &&
|
|
169
|
+
artifact.sessionId === input.sessionId &&
|
|
170
|
+
(input.workflowRunId === undefined || artifact.workflowRunId === input.workflowRunId) &&
|
|
171
|
+
(input.workflowStepId === undefined || artifact.workflowStepId === input.workflowStepId) &&
|
|
172
|
+
(input.agentRunId === undefined || artifact.agentRunId === input.agentRunId);
|
|
173
|
+
const fileNameForArtifact = (input) => [input.appName, input.userId, input.sessionId, input.id].map((part) => encodeURIComponent(part)).join("__") + ".json";
|
|
174
|
+
const blobPathForArtifact = (input) => path.join("blobs", [input.appName, input.userId, input.sessionId, input.id].map((part) => encodeURIComponent(part)).join("__") + ".bin");
|
|
175
|
+
const lookupFromArtifact = (artifact) => ({
|
|
176
|
+
appName: artifact.appName,
|
|
177
|
+
userId: artifact.userId,
|
|
178
|
+
sessionId: artifact.sessionId,
|
|
179
|
+
id: artifact.id
|
|
180
|
+
});
|
|
181
|
+
export const createBase64ArtifactData = (input) => {
|
|
182
|
+
const data = typeof input === "object" && "data" in input ? input.data : input;
|
|
183
|
+
const buffer = typeof data === "string"
|
|
184
|
+
? Buffer.from(data, "utf8")
|
|
185
|
+
: data instanceof Uint8Array
|
|
186
|
+
? Buffer.from(data)
|
|
187
|
+
: Buffer.from(data);
|
|
188
|
+
return {
|
|
189
|
+
data: buffer.toString("base64"),
|
|
190
|
+
encoding: "base64",
|
|
191
|
+
size: buffer.byteLength
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
export const createExternalArtifactReference = (input) => {
|
|
195
|
+
validateArtifactMetadata(input);
|
|
196
|
+
if (!input.uri) {
|
|
197
|
+
throw new ValidationError('The "uri" external artifact reference option is required.');
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
data: null,
|
|
201
|
+
storageMode: "binary",
|
|
202
|
+
size: input.size,
|
|
203
|
+
sha256: input.sha256,
|
|
204
|
+
metadata: {
|
|
205
|
+
...(input.metadata ?? {}),
|
|
206
|
+
externalBlob: {
|
|
207
|
+
uri: input.uri,
|
|
208
|
+
managedBy: "application"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
export const verifyArtifactRecordIntegrity = (record, data) => {
|
|
214
|
+
const artifact = normalizeArtifactRecord(record);
|
|
215
|
+
const issues = [];
|
|
216
|
+
let bytes = data;
|
|
217
|
+
if (!bytes && artifact.encoding === "base64" && typeof artifact.data === "string") {
|
|
218
|
+
try {
|
|
219
|
+
bytes = new Uint8Array(Buffer.from(artifact.data, "base64"));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
issues.push({
|
|
223
|
+
type: "invalid-base64",
|
|
224
|
+
message: `Artifact "${artifact.id}" contains invalid base64 data.`
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (bytes) {
|
|
229
|
+
if (artifact.size !== undefined && artifact.size !== bytes.byteLength) {
|
|
230
|
+
issues.push({
|
|
231
|
+
type: "size-mismatch",
|
|
232
|
+
message: `Artifact "${artifact.id}" size does not match.`,
|
|
233
|
+
expected: artifact.size,
|
|
234
|
+
actual: bytes.byteLength
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (artifact.sha256 !== undefined) {
|
|
238
|
+
const actual = sha256Digest(bytes);
|
|
239
|
+
if (artifact.sha256 !== actual) {
|
|
240
|
+
issues.push({
|
|
241
|
+
type: "sha256-mismatch",
|
|
242
|
+
message: `Artifact "${artifact.id}" sha256 does not match.`,
|
|
243
|
+
expected: artifact.sha256,
|
|
244
|
+
actual
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
ok: issues.length === 0,
|
|
251
|
+
artifact,
|
|
252
|
+
issues
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
export const verifyArtifactIntegrity = async (service, lookup) => {
|
|
256
|
+
const artifact = await service.loadArtifact(lookup);
|
|
257
|
+
if (!artifact) {
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
issues: [{
|
|
261
|
+
type: "missing-artifact",
|
|
262
|
+
message: `Artifact "${lookup.id}" was not found.`
|
|
263
|
+
}]
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (artifact.storageMode === "binary") {
|
|
267
|
+
const binary = await service.loadBinaryArtifact(lookup);
|
|
268
|
+
if (!binary) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
artifact,
|
|
272
|
+
issues: [{
|
|
273
|
+
type: "missing-blob",
|
|
274
|
+
message: `Artifact "${lookup.id}" binary blob was not found.`
|
|
275
|
+
}]
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return verifyArtifactRecordIntegrity(binary.artifact, binary.data);
|
|
279
|
+
}
|
|
280
|
+
return verifyArtifactRecordIntegrity(artifact);
|
|
281
|
+
};
|
|
282
|
+
const listFilesRecursive = async (directory) => {
|
|
283
|
+
try {
|
|
284
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
285
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
286
|
+
const fullPath = path.join(directory, entry.name);
|
|
287
|
+
if (entry.isDirectory()) {
|
|
288
|
+
return listFilesRecursive(fullPath);
|
|
289
|
+
}
|
|
290
|
+
return [fullPath];
|
|
291
|
+
}));
|
|
292
|
+
return files.flat();
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (error.code === "ENOENT") {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
export const inspectFileArtifactStore = async (options) => {
|
|
302
|
+
const artifacts = [];
|
|
303
|
+
const issues = [];
|
|
304
|
+
const referencedBlobPaths = new Set();
|
|
305
|
+
let entries = [];
|
|
306
|
+
try {
|
|
307
|
+
entries = await fs.readdir(options.directory);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (error.code !== "ENOENT") {
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
if (!entry.endsWith(".json")) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const metadataPath = path.join(options.directory, entry);
|
|
319
|
+
try {
|
|
320
|
+
const artifact = normalizeArtifactRecord(JSON.parse(await fs.readFile(metadataPath, "utf8")));
|
|
321
|
+
artifacts.push(artifact);
|
|
322
|
+
if (artifact.blobPath) {
|
|
323
|
+
referencedBlobPaths.add(path.normalize(artifact.blobPath));
|
|
324
|
+
}
|
|
325
|
+
if (artifact.storageMode === "binary") {
|
|
326
|
+
if (!artifact.blobPath) {
|
|
327
|
+
issues.push({
|
|
328
|
+
type: "missing-blob",
|
|
329
|
+
path: metadataPath,
|
|
330
|
+
artifact,
|
|
331
|
+
message: `Artifact "${artifact.id}" has no blobPath.`
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const fullBlobPath = path.join(options.directory, artifact.blobPath);
|
|
336
|
+
try {
|
|
337
|
+
await fs.stat(fullBlobPath);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
if (error.code === "ENOENT") {
|
|
341
|
+
issues.push({
|
|
342
|
+
type: "missing-blob",
|
|
343
|
+
path: fullBlobPath,
|
|
344
|
+
artifact,
|
|
345
|
+
message: `Artifact "${artifact.id}" references a missing blob.`
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
issues.push({
|
|
357
|
+
type: "invalid-metadata",
|
|
358
|
+
path: metadataPath,
|
|
359
|
+
message: error instanceof Error ? error.message : String(error)
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const blobRoot = path.join(options.directory, "blobs");
|
|
364
|
+
for (const blobFile of await listFilesRecursive(blobRoot)) {
|
|
365
|
+
const relative = path.normalize(path.relative(options.directory, blobFile));
|
|
366
|
+
if (!referencedBlobPaths.has(relative)) {
|
|
367
|
+
issues.push({
|
|
368
|
+
type: "orphan-blob",
|
|
369
|
+
path: blobFile,
|
|
370
|
+
message: `Blob "${relative}" is not referenced by artifact metadata.`
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
directory: options.directory,
|
|
376
|
+
artifacts: artifacts.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
|
|
377
|
+
issues
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
export const cleanupFileArtifactStore = async (options) => {
|
|
381
|
+
const inspection = await inspectFileArtifactStore(options);
|
|
382
|
+
const deletedBlobPaths = [];
|
|
383
|
+
for (const issue of inspection.issues) {
|
|
384
|
+
if (issue.type !== "orphan-blob") {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!options.dryRun) {
|
|
388
|
+
await fs.unlink(issue.path);
|
|
389
|
+
}
|
|
390
|
+
deletedBlobPaths.push(issue.path);
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
...inspection,
|
|
394
|
+
dryRun: Boolean(options.dryRun),
|
|
395
|
+
deletedBlobPaths
|
|
396
|
+
};
|
|
397
|
+
};
|
|
398
|
+
export const pruneFileArtifactStore = async (options) => {
|
|
399
|
+
const now = options.now ?? Date.now();
|
|
400
|
+
const dryRun = options.dryRun ?? true;
|
|
401
|
+
const inspection = await inspectFileArtifactStore({ directory: options.directory });
|
|
402
|
+
const sorted = inspection.artifacts.sort((left, right) => right.updatedAt - left.updatedAt || artifactKey(left).localeCompare(artifactKey(right)));
|
|
403
|
+
const keepByCount = new Set(options.keepLast === undefined ? [] : sorted.slice(0, Math.max(0, options.keepLast)).map((artifact) => artifactKey(artifact)));
|
|
404
|
+
const shouldDelete = (artifact) => !keepByCount.has(artifactKey(artifact)) &&
|
|
405
|
+
(options.olderThanMs !== undefined ? now - artifact.updatedAt > options.olderThanMs : options.keepLast !== undefined);
|
|
406
|
+
const deleted = sorted.filter(shouldDelete);
|
|
407
|
+
const deletedBlobPaths = deleted.flatMap((artifact) => artifact.blobPath ? [artifact.blobPath] : []);
|
|
408
|
+
if (!dryRun) {
|
|
409
|
+
for (const artifact of deleted) {
|
|
410
|
+
await fs.unlink(path.join(options.directory, fileNameForArtifact(artifact))).catch((error) => {
|
|
411
|
+
if (error.code !== "ENOENT") {
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
if (artifact.blobPath) {
|
|
416
|
+
await fs.unlink(path.join(options.directory, artifact.blobPath)).catch((error) => {
|
|
417
|
+
if (error.code !== "ENOENT") {
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
directory: options.directory,
|
|
426
|
+
dryRun,
|
|
427
|
+
deletedArtifactKeys: deleted.map((artifact) => artifactKey(artifact)),
|
|
428
|
+
keptArtifactKeys: sorted.filter((artifact) => !shouldDelete(artifact)).map((artifact) => artifactKey(artifact)),
|
|
429
|
+
deletedBlobPaths
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
export const createInMemoryArtifactService = () => {
|
|
433
|
+
const artifacts = new Map();
|
|
434
|
+
const binaryData = new Map();
|
|
435
|
+
return {
|
|
436
|
+
saveArtifact(input) {
|
|
437
|
+
const id = input.id ?? randomId("art");
|
|
438
|
+
const lookup = {
|
|
439
|
+
appName: input.appName,
|
|
440
|
+
userId: input.userId,
|
|
441
|
+
sessionId: input.sessionId,
|
|
442
|
+
id
|
|
443
|
+
};
|
|
444
|
+
const existing = artifacts.get(artifactKey(lookup));
|
|
445
|
+
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
446
|
+
const artifact = createArtifact({ ...input, id }, existing);
|
|
447
|
+
artifacts.set(artifactKey(lookup), cloneArtifact(artifact));
|
|
448
|
+
binaryData.delete(artifactKey(lookup));
|
|
449
|
+
return cloneArtifact(artifact);
|
|
450
|
+
},
|
|
451
|
+
saveBinaryArtifact(input) {
|
|
452
|
+
const id = input.id ?? randomId("art");
|
|
453
|
+
const lookup = {
|
|
454
|
+
appName: input.appName,
|
|
455
|
+
userId: input.userId,
|
|
456
|
+
sessionId: input.sessionId,
|
|
457
|
+
id
|
|
458
|
+
};
|
|
459
|
+
const bytes = bytesFromBinaryInput(input.data);
|
|
460
|
+
const sha256 = input.sha256 ?? sha256Digest(bytes);
|
|
461
|
+
const existing = artifacts.get(artifactKey(lookup));
|
|
462
|
+
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
463
|
+
const artifact = createArtifact({
|
|
464
|
+
...input,
|
|
465
|
+
id,
|
|
466
|
+
data: null,
|
|
467
|
+
encoding: "base64",
|
|
468
|
+
size: bytes.byteLength,
|
|
469
|
+
sha256,
|
|
470
|
+
storageMode: "binary"
|
|
471
|
+
}, existing);
|
|
472
|
+
artifacts.set(artifactKey(lookup), cloneArtifact(artifact));
|
|
473
|
+
binaryData.set(artifactKey(lookup), new Uint8Array(bytes));
|
|
474
|
+
return cloneArtifact(artifact);
|
|
475
|
+
},
|
|
476
|
+
loadArtifact(input) {
|
|
477
|
+
const artifact = artifacts.get(artifactKey(input));
|
|
478
|
+
return artifact ? cloneArtifact(artifact) : undefined;
|
|
479
|
+
},
|
|
480
|
+
loadBinaryArtifact(input) {
|
|
481
|
+
const artifact = artifacts.get(artifactKey(input));
|
|
482
|
+
const data = binaryData.get(artifactKey(input));
|
|
483
|
+
if (!artifact || !data) {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
artifact: cloneArtifact(artifact),
|
|
488
|
+
data: new Uint8Array(data)
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
listArtifacts(input) {
|
|
492
|
+
return [...artifacts.values()]
|
|
493
|
+
.filter((artifact) => matchesListInput(artifact, input))
|
|
494
|
+
.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id))
|
|
495
|
+
.map(cloneArtifact);
|
|
496
|
+
},
|
|
497
|
+
deleteArtifact(input) {
|
|
498
|
+
artifacts.delete(artifactKey(input));
|
|
499
|
+
binaryData.delete(artifactKey(input));
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
};
|
|
503
|
+
export const createFileArtifactService = (options) => {
|
|
504
|
+
const filePath = (input) => path.join(options.directory, fileNameForArtifact(input));
|
|
505
|
+
const binaryPath = (input) => path.join(options.directory, blobPathForArtifact(input));
|
|
506
|
+
const load = async (input) => {
|
|
507
|
+
try {
|
|
508
|
+
const content = await fs.readFile(filePath(input), "utf8");
|
|
509
|
+
return normalizeArtifactRecord(JSON.parse(content));
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
if (error.code === "ENOENT") {
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const save = async (artifact) => {
|
|
519
|
+
await fs.mkdir(options.directory, { recursive: true });
|
|
520
|
+
await fs.writeFile(filePath(artifact), JSON.stringify(cloneArtifact(artifact), null, 2), "utf8");
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
async saveArtifact(input) {
|
|
524
|
+
const id = input.id ?? randomId("art");
|
|
525
|
+
const lookup = {
|
|
526
|
+
appName: input.appName,
|
|
527
|
+
userId: input.userId,
|
|
528
|
+
sessionId: input.sessionId,
|
|
529
|
+
id
|
|
530
|
+
};
|
|
531
|
+
const existing = await load(lookup);
|
|
532
|
+
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
533
|
+
const artifact = createArtifact({ ...input, id }, existing);
|
|
534
|
+
await save(artifact);
|
|
535
|
+
const existingBlob = binaryPath(lookup);
|
|
536
|
+
await fs.unlink(existingBlob).catch((error) => {
|
|
537
|
+
if (error.code !== "ENOENT") {
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
return cloneArtifact(artifact);
|
|
542
|
+
},
|
|
543
|
+
async saveBinaryArtifact(input) {
|
|
544
|
+
const id = input.id ?? randomId("art");
|
|
545
|
+
const lookup = {
|
|
546
|
+
appName: input.appName,
|
|
547
|
+
userId: input.userId,
|
|
548
|
+
sessionId: input.sessionId,
|
|
549
|
+
id
|
|
550
|
+
};
|
|
551
|
+
const bytes = bytesFromBinaryInput(input.data);
|
|
552
|
+
const sha256 = input.sha256 ?? sha256Digest(bytes);
|
|
553
|
+
const blobPath = blobPathForArtifact(lookup);
|
|
554
|
+
const existing = await load(lookup);
|
|
555
|
+
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
556
|
+
const artifact = createArtifact({
|
|
557
|
+
...input,
|
|
558
|
+
id,
|
|
559
|
+
data: null,
|
|
560
|
+
encoding: "base64",
|
|
561
|
+
size: bytes.byteLength,
|
|
562
|
+
sha256,
|
|
563
|
+
storageMode: "binary",
|
|
564
|
+
blobPath
|
|
565
|
+
}, existing);
|
|
566
|
+
await fs.mkdir(path.dirname(binaryPath(lookup)), { recursive: true });
|
|
567
|
+
await fs.writeFile(binaryPath(lookup), bytes);
|
|
568
|
+
await save(artifact);
|
|
569
|
+
return cloneArtifact(artifact);
|
|
570
|
+
},
|
|
571
|
+
async loadArtifact(input) {
|
|
572
|
+
return load(input);
|
|
573
|
+
},
|
|
574
|
+
async loadBinaryArtifact(input) {
|
|
575
|
+
const artifact = await load(input);
|
|
576
|
+
if (!artifact || artifact.storageMode !== "binary" || !artifact.blobPath) {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
const data = await fs.readFile(path.join(options.directory, artifact.blobPath));
|
|
581
|
+
return {
|
|
582
|
+
artifact,
|
|
583
|
+
data: new Uint8Array(data)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
if (error.code === "ENOENT") {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
async listArtifacts(input) {
|
|
594
|
+
let entries;
|
|
595
|
+
try {
|
|
596
|
+
entries = await fs.readdir(options.directory);
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
if (error.code === "ENOENT") {
|
|
600
|
+
return [];
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
const artifacts = [];
|
|
605
|
+
for (const entry of entries) {
|
|
606
|
+
if (!entry.endsWith(".json")) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const content = await fs.readFile(path.join(options.directory, entry), "utf8");
|
|
610
|
+
const artifact = normalizeArtifactRecord(JSON.parse(content));
|
|
611
|
+
if (matchesListInput(artifact, input)) {
|
|
612
|
+
artifacts.push(cloneArtifact(artifact));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return artifacts.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
|
|
616
|
+
},
|
|
617
|
+
async deleteArtifact(input) {
|
|
618
|
+
const artifact = await load(input);
|
|
619
|
+
try {
|
|
620
|
+
await fs.unlink(filePath(input));
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
if (error.code !== "ENOENT") {
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (artifact?.blobPath) {
|
|
628
|
+
try {
|
|
629
|
+
await fs.unlink(path.join(options.directory, artifact.blobPath));
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
if (error.code !== "ENOENT") {
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
};
|
|
640
|
+
export const createSqliteArtifactService = (options) => {
|
|
641
|
+
const tableName = validateIdentifier(options.tableName ?? "zhivex_artifacts", "tableName");
|
|
642
|
+
options.db.exec(`
|
|
643
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
644
|
+
artifact_key TEXT PRIMARY KEY,
|
|
645
|
+
app_name TEXT NOT NULL,
|
|
646
|
+
user_id TEXT NOT NULL,
|
|
647
|
+
session_id TEXT NOT NULL,
|
|
648
|
+
artifact_id TEXT NOT NULL,
|
|
649
|
+
workflow_run_id TEXT,
|
|
650
|
+
workflow_step_id TEXT,
|
|
651
|
+
agent_run_id TEXT,
|
|
652
|
+
artifact_json TEXT NOT NULL,
|
|
653
|
+
created_at_ms INTEGER NOT NULL,
|
|
654
|
+
updated_at_ms INTEGER NOT NULL
|
|
655
|
+
)
|
|
656
|
+
`);
|
|
657
|
+
const loadStatement = prepareSqliteStatement(options.db, `SELECT artifact_json FROM ${tableName} WHERE artifact_key = ?`);
|
|
658
|
+
const listStatement = prepareSqliteStatement(options.db, `SELECT artifact_json FROM ${tableName}
|
|
659
|
+
WHERE app_name = ?
|
|
660
|
+
AND user_id = ?
|
|
661
|
+
AND session_id = ?
|
|
662
|
+
AND (? IS NULL OR workflow_run_id = ?)
|
|
663
|
+
AND (? IS NULL OR workflow_step_id = ?)
|
|
664
|
+
AND (? IS NULL OR agent_run_id = ?)
|
|
665
|
+
ORDER BY created_at_ms ASC, artifact_id ASC`);
|
|
666
|
+
const saveStatement = prepareSqliteStatement(options.db, `
|
|
667
|
+
INSERT INTO ${tableName} (
|
|
668
|
+
artifact_key,
|
|
669
|
+
app_name,
|
|
670
|
+
user_id,
|
|
671
|
+
session_id,
|
|
672
|
+
artifact_id,
|
|
673
|
+
workflow_run_id,
|
|
674
|
+
workflow_step_id,
|
|
675
|
+
agent_run_id,
|
|
676
|
+
artifact_json,
|
|
677
|
+
created_at_ms,
|
|
678
|
+
updated_at_ms
|
|
679
|
+
)
|
|
680
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
681
|
+
ON CONFLICT(artifact_key) DO UPDATE SET
|
|
682
|
+
app_name = excluded.app_name,
|
|
683
|
+
user_id = excluded.user_id,
|
|
684
|
+
session_id = excluded.session_id,
|
|
685
|
+
artifact_id = excluded.artifact_id,
|
|
686
|
+
workflow_run_id = excluded.workflow_run_id,
|
|
687
|
+
workflow_step_id = excluded.workflow_step_id,
|
|
688
|
+
agent_run_id = excluded.agent_run_id,
|
|
689
|
+
artifact_json = excluded.artifact_json,
|
|
690
|
+
updated_at_ms = excluded.updated_at_ms
|
|
691
|
+
`);
|
|
692
|
+
const updateCasStatement = prepareSqliteStatement(options.db, `
|
|
693
|
+
UPDATE ${tableName}
|
|
694
|
+
SET app_name = ?,
|
|
695
|
+
user_id = ?,
|
|
696
|
+
session_id = ?,
|
|
697
|
+
artifact_id = ?,
|
|
698
|
+
workflow_run_id = ?,
|
|
699
|
+
workflow_step_id = ?,
|
|
700
|
+
agent_run_id = ?,
|
|
701
|
+
artifact_json = ?,
|
|
702
|
+
updated_at_ms = ?
|
|
703
|
+
WHERE artifact_key = ?
|
|
704
|
+
AND updated_at_ms = ?
|
|
705
|
+
`);
|
|
706
|
+
const deleteStatement = prepareSqliteStatement(options.db, `DELETE FROM ${tableName} WHERE artifact_key = ?`);
|
|
707
|
+
const load = (input) => {
|
|
708
|
+
const row = loadStatement.get([artifactKey(input)]);
|
|
709
|
+
return parseArtifactJson(getRecordField(row, ["artifact_json", "artifactJson"]));
|
|
710
|
+
};
|
|
711
|
+
const save = (artifact, options) => {
|
|
712
|
+
if (options?.expectedRevision !== undefined && options.existing) {
|
|
713
|
+
const result = updateCasStatement.run([
|
|
714
|
+
artifact.appName,
|
|
715
|
+
artifact.userId,
|
|
716
|
+
artifact.sessionId,
|
|
717
|
+
artifact.id,
|
|
718
|
+
artifact.workflowRunId ?? null,
|
|
719
|
+
artifact.workflowStepId ?? null,
|
|
720
|
+
artifact.agentRunId ?? null,
|
|
721
|
+
JSON.stringify(artifact),
|
|
722
|
+
artifact.updatedAt,
|
|
723
|
+
artifactKey(lookupFromArtifact(artifact)),
|
|
724
|
+
options.existing.updatedAt
|
|
725
|
+
]);
|
|
726
|
+
if (sqliteMutationCount(result) === 0) {
|
|
727
|
+
throw new ConflictError("ArtifactRecord revision conflict.");
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
saveStatement.run([
|
|
732
|
+
artifactKey(lookupFromArtifact(artifact)),
|
|
733
|
+
artifact.appName,
|
|
734
|
+
artifact.userId,
|
|
735
|
+
artifact.sessionId,
|
|
736
|
+
artifact.id,
|
|
737
|
+
artifact.workflowRunId ?? null,
|
|
738
|
+
artifact.workflowStepId ?? null,
|
|
739
|
+
artifact.agentRunId ?? null,
|
|
740
|
+
JSON.stringify(artifact),
|
|
741
|
+
artifact.createdAt,
|
|
742
|
+
artifact.updatedAt
|
|
743
|
+
]);
|
|
744
|
+
}
|
|
745
|
+
return cloneArtifact(artifact);
|
|
746
|
+
};
|
|
747
|
+
return {
|
|
748
|
+
saveArtifact(input) {
|
|
749
|
+
const id = input.id ?? randomId("art");
|
|
750
|
+
const existing = load({
|
|
751
|
+
appName: input.appName,
|
|
752
|
+
userId: input.userId,
|
|
753
|
+
sessionId: input.sessionId,
|
|
754
|
+
id
|
|
755
|
+
});
|
|
756
|
+
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
757
|
+
return save(createArtifact({ ...input, id }, existing), {
|
|
758
|
+
existing,
|
|
759
|
+
expectedRevision: input.expectedRevision
|
|
760
|
+
});
|
|
761
|
+
},
|
|
762
|
+
saveBinaryArtifact(input) {
|
|
763
|
+
const id = input.id ?? randomId("art");
|
|
764
|
+
const bytes = bytesFromBinaryInput(input.data);
|
|
765
|
+
const existing = load({
|
|
766
|
+
appName: input.appName,
|
|
767
|
+
userId: input.userId,
|
|
768
|
+
sessionId: input.sessionId,
|
|
769
|
+
id
|
|
770
|
+
});
|
|
771
|
+
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
772
|
+
return save(createArtifact({
|
|
773
|
+
...input,
|
|
774
|
+
id,
|
|
775
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
776
|
+
encoding: "base64",
|
|
777
|
+
size: bytes.byteLength,
|
|
778
|
+
sha256: input.sha256 ?? sha256Digest(bytes),
|
|
779
|
+
storageMode: "json"
|
|
780
|
+
}, existing), {
|
|
781
|
+
existing,
|
|
782
|
+
expectedRevision: input.expectedRevision
|
|
783
|
+
});
|
|
784
|
+
},
|
|
785
|
+
loadArtifact(input) {
|
|
786
|
+
return load(input);
|
|
787
|
+
},
|
|
788
|
+
loadBinaryArtifact(input) {
|
|
789
|
+
const artifact = load(input);
|
|
790
|
+
if (!artifact || artifact.encoding !== "base64" || typeof artifact.data !== "string") {
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
artifact,
|
|
795
|
+
data: new Uint8Array(Buffer.from(artifact.data, "base64"))
|
|
796
|
+
};
|
|
797
|
+
},
|
|
798
|
+
listArtifacts(input) {
|
|
799
|
+
const params = [
|
|
800
|
+
input.appName,
|
|
801
|
+
input.userId,
|
|
802
|
+
input.sessionId,
|
|
803
|
+
input.workflowRunId ?? null,
|
|
804
|
+
input.workflowRunId ?? null,
|
|
805
|
+
input.workflowStepId ?? null,
|
|
806
|
+
input.workflowStepId ?? null,
|
|
807
|
+
input.agentRunId ?? null,
|
|
808
|
+
input.agentRunId ?? null
|
|
809
|
+
];
|
|
810
|
+
const rows = listStatement.all?.(params) ?? [];
|
|
811
|
+
return rows.flatMap((row) => {
|
|
812
|
+
const artifact = parseArtifactJson(getRecordField(row, ["artifact_json", "artifactJson"]));
|
|
813
|
+
return artifact ? [artifact] : [];
|
|
814
|
+
});
|
|
815
|
+
},
|
|
816
|
+
deleteArtifact(input) {
|
|
817
|
+
deleteStatement.run([artifactKey(input)]);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
};
|
|
821
|
+
export const createPostgresArtifactService = (options) => {
|
|
822
|
+
const tableName = validateIdentifier(options.tableName ?? "zhivex_artifacts", "tableName");
|
|
823
|
+
const createSql = `
|
|
824
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
825
|
+
artifact_key TEXT PRIMARY KEY,
|
|
826
|
+
app_name TEXT NOT NULL,
|
|
827
|
+
user_id TEXT NOT NULL,
|
|
828
|
+
session_id TEXT NOT NULL,
|
|
829
|
+
artifact_id TEXT NOT NULL,
|
|
830
|
+
workflow_run_id TEXT,
|
|
831
|
+
workflow_step_id TEXT,
|
|
832
|
+
agent_run_id TEXT,
|
|
833
|
+
artifact_json JSONB NOT NULL,
|
|
834
|
+
created_at_ms BIGINT NOT NULL,
|
|
835
|
+
updated_at_ms BIGINT NOT NULL
|
|
836
|
+
)
|
|
837
|
+
`;
|
|
838
|
+
const load = async (input) => {
|
|
839
|
+
await ensurePostgresTable(options.client, tableName, createSql);
|
|
840
|
+
const result = await options.client.query(`SELECT artifact_json FROM ${tableName} WHERE artifact_key = $1`, [artifactKey(input)]);
|
|
841
|
+
return parseArtifactJson(getRecordField(result.rows[0], ["artifact_json", "artifactJson"]));
|
|
842
|
+
};
|
|
843
|
+
const save = async (artifact, saveOptions) => {
|
|
844
|
+
await ensurePostgresTable(options.client, tableName, createSql);
|
|
845
|
+
if (saveOptions?.expectedRevision !== undefined && saveOptions.existing) {
|
|
846
|
+
const result = await options.client.query(`UPDATE ${tableName}
|
|
847
|
+
SET app_name = $2,
|
|
848
|
+
user_id = $3,
|
|
849
|
+
session_id = $4,
|
|
850
|
+
artifact_id = $5,
|
|
851
|
+
workflow_run_id = $6,
|
|
852
|
+
workflow_step_id = $7,
|
|
853
|
+
agent_run_id = $8,
|
|
854
|
+
artifact_json = $9::jsonb,
|
|
855
|
+
updated_at_ms = $10
|
|
856
|
+
WHERE artifact_key = $1
|
|
857
|
+
AND updated_at_ms = $11
|
|
858
|
+
RETURNING artifact_json`, [
|
|
859
|
+
artifactKey(lookupFromArtifact(artifact)),
|
|
860
|
+
artifact.appName,
|
|
861
|
+
artifact.userId,
|
|
862
|
+
artifact.sessionId,
|
|
863
|
+
artifact.id,
|
|
864
|
+
artifact.workflowRunId ?? null,
|
|
865
|
+
artifact.workflowStepId ?? null,
|
|
866
|
+
artifact.agentRunId ?? null,
|
|
867
|
+
JSON.stringify(artifact),
|
|
868
|
+
artifact.updatedAt,
|
|
869
|
+
saveOptions.existing.updatedAt
|
|
870
|
+
]);
|
|
871
|
+
if (result.rows.length === 0) {
|
|
872
|
+
throw new ConflictError("ArtifactRecord revision conflict.");
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
await options.client.query(`INSERT INTO ${tableName} (
|
|
877
|
+
artifact_key,
|
|
878
|
+
app_name,
|
|
879
|
+
user_id,
|
|
880
|
+
session_id,
|
|
881
|
+
artifact_id,
|
|
882
|
+
workflow_run_id,
|
|
883
|
+
workflow_step_id,
|
|
884
|
+
agent_run_id,
|
|
885
|
+
artifact_json,
|
|
886
|
+
created_at_ms,
|
|
887
|
+
updated_at_ms
|
|
888
|
+
)
|
|
889
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11)
|
|
890
|
+
ON CONFLICT(artifact_key) DO UPDATE SET
|
|
891
|
+
app_name = EXCLUDED.app_name,
|
|
892
|
+
user_id = EXCLUDED.user_id,
|
|
893
|
+
session_id = EXCLUDED.session_id,
|
|
894
|
+
artifact_id = EXCLUDED.artifact_id,
|
|
895
|
+
workflow_run_id = EXCLUDED.workflow_run_id,
|
|
896
|
+
workflow_step_id = EXCLUDED.workflow_step_id,
|
|
897
|
+
agent_run_id = EXCLUDED.agent_run_id,
|
|
898
|
+
artifact_json = EXCLUDED.artifact_json,
|
|
899
|
+
updated_at_ms = EXCLUDED.updated_at_ms`, [
|
|
900
|
+
artifactKey(lookupFromArtifact(artifact)),
|
|
901
|
+
artifact.appName,
|
|
902
|
+
artifact.userId,
|
|
903
|
+
artifact.sessionId,
|
|
904
|
+
artifact.id,
|
|
905
|
+
artifact.workflowRunId ?? null,
|
|
906
|
+
artifact.workflowStepId ?? null,
|
|
907
|
+
artifact.agentRunId ?? null,
|
|
908
|
+
JSON.stringify(artifact),
|
|
909
|
+
artifact.createdAt,
|
|
910
|
+
artifact.updatedAt
|
|
911
|
+
]);
|
|
912
|
+
}
|
|
913
|
+
return cloneArtifact(artifact);
|
|
914
|
+
};
|
|
915
|
+
return {
|
|
916
|
+
async saveArtifact(input) {
|
|
917
|
+
const id = input.id ?? randomId("art");
|
|
918
|
+
const existing = await load({
|
|
919
|
+
appName: input.appName,
|
|
920
|
+
userId: input.userId,
|
|
921
|
+
sessionId: input.sessionId,
|
|
922
|
+
id
|
|
923
|
+
});
|
|
924
|
+
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
925
|
+
return save(createArtifact({ ...input, id }, existing), {
|
|
926
|
+
existing,
|
|
927
|
+
expectedRevision: input.expectedRevision
|
|
928
|
+
});
|
|
929
|
+
},
|
|
930
|
+
async saveBinaryArtifact(input) {
|
|
931
|
+
const id = input.id ?? randomId("art");
|
|
932
|
+
const bytes = bytesFromBinaryInput(input.data);
|
|
933
|
+
const existing = await load({
|
|
934
|
+
appName: input.appName,
|
|
935
|
+
userId: input.userId,
|
|
936
|
+
sessionId: input.sessionId,
|
|
937
|
+
id
|
|
938
|
+
});
|
|
939
|
+
assertExpectedRevision(existing, input.expectedRevision, "ArtifactRecord");
|
|
940
|
+
return save(createArtifact({
|
|
941
|
+
...input,
|
|
942
|
+
id,
|
|
943
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
944
|
+
encoding: "base64",
|
|
945
|
+
size: bytes.byteLength,
|
|
946
|
+
sha256: input.sha256 ?? sha256Digest(bytes),
|
|
947
|
+
storageMode: "json"
|
|
948
|
+
}, existing), {
|
|
949
|
+
existing,
|
|
950
|
+
expectedRevision: input.expectedRevision
|
|
951
|
+
});
|
|
952
|
+
},
|
|
953
|
+
loadArtifact(input) {
|
|
954
|
+
return load(input);
|
|
955
|
+
},
|
|
956
|
+
async loadBinaryArtifact(input) {
|
|
957
|
+
const artifact = await load(input);
|
|
958
|
+
if (!artifact || artifact.encoding !== "base64" || typeof artifact.data !== "string") {
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
artifact,
|
|
963
|
+
data: new Uint8Array(Buffer.from(artifact.data, "base64"))
|
|
964
|
+
};
|
|
965
|
+
},
|
|
966
|
+
async listArtifacts(input) {
|
|
967
|
+
await ensurePostgresTable(options.client, tableName, createSql);
|
|
968
|
+
const result = await options.client.query(`SELECT artifact_json FROM ${tableName}
|
|
969
|
+
WHERE app_name = $1
|
|
970
|
+
AND user_id = $2
|
|
971
|
+
AND session_id = $3
|
|
972
|
+
AND ($4::text IS NULL OR workflow_run_id = $4)
|
|
973
|
+
AND ($5::text IS NULL OR workflow_step_id = $5)
|
|
974
|
+
AND ($6::text IS NULL OR agent_run_id = $6)
|
|
975
|
+
ORDER BY created_at_ms ASC, artifact_id ASC`, [
|
|
976
|
+
input.appName,
|
|
977
|
+
input.userId,
|
|
978
|
+
input.sessionId,
|
|
979
|
+
input.workflowRunId ?? null,
|
|
980
|
+
input.workflowStepId ?? null,
|
|
981
|
+
input.agentRunId ?? null
|
|
982
|
+
]);
|
|
983
|
+
return result.rows.flatMap((row) => {
|
|
984
|
+
const artifact = parseArtifactJson(getRecordField(row, ["artifact_json", "artifactJson"]));
|
|
985
|
+
return artifact ? [artifact] : [];
|
|
986
|
+
});
|
|
987
|
+
},
|
|
988
|
+
async deleteArtifact(input) {
|
|
989
|
+
await ensurePostgresTable(options.client, tableName, createSql);
|
|
990
|
+
await options.client.query(`DELETE FROM ${tableName} WHERE artifact_key = $1`, [artifactKey(input)]);
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
};
|
|
994
|
+
//# sourceMappingURL=artifact.js.map
|