@zhivex-ai/core 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-handoff.d.ts +2 -0
- package/dist/agent-handoff.d.ts.map +1 -1
- package/dist/agent-handoff.js +8 -3
- package/dist/agent-handoff.js.map +1 -1
- package/dist/agent-state.d.ts +6 -0
- package/dist/agent-state.d.ts.map +1 -0
- package/dist/agent-state.js +374 -0
- package/dist/agent-state.js.map +1 -0
- package/dist/agent-store.d.ts +5 -3
- package/dist/agent-store.d.ts.map +1 -1
- package/dist/agent-store.js +1027 -145
- package/dist/agent-store.js.map +1 -1
- package/dist/agent.d.ts +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +698 -229
- package/dist/agent.js.map +1 -1
- package/dist/api-stability.d.ts.map +1 -1
- package/dist/api-stability.js +10 -0
- package/dist/api-stability.js.map +1 -1
- package/dist/artifact.d.ts.map +1 -1
- package/dist/artifact.js +2 -1
- package/dist/artifact.js.map +1 -1
- package/dist/audio.d.ts +2 -1
- package/dist/audio.d.ts.map +1 -1
- package/dist/audio.js +25 -0
- package/dist/audio.js.map +1 -1
- package/dist/bounded-broadcast.d.ts +45 -0
- package/dist/bounded-broadcast.d.ts.map +1 -0
- package/dist/bounded-broadcast.js +163 -0
- package/dist/bounded-broadcast.js.map +1 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +119 -14
- package/dist/catalog.js.map +1 -1
- package/dist/generate-object.d.ts.map +1 -1
- package/dist/generate-object.js +26 -88
- package/dist/generate-object.js.map +1 -1
- package/dist/generate-text.d.ts +9 -1
- package/dist/generate-text.d.ts.map +1 -1
- package/dist/generate-text.js +118 -55
- package/dist/generate-text.js.map +1 -1
- package/dist/google.d.ts +12 -1
- package/dist/google.d.ts.map +1 -1
- package/dist/google.js +27 -0
- package/dist/google.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/live-agent.d.ts.map +1 -1
- package/dist/live-agent.js +31 -54
- package/dist/live-agent.js.map +1 -1
- package/dist/messages.d.ts +4 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js.map +1 -1
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +39 -55
- package/dist/realtime.js.map +1 -1
- package/dist/response.js +1 -1
- package/dist/response.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +2 -1
- package/dist/runner.js.map +1 -1
- package/dist/safety-policy.d.ts +41 -1
- package/dist/safety-policy.d.ts.map +1 -1
- package/dist/safety-policy.js +114 -8
- package/dist/safety-policy.js.map +1 -1
- package/dist/secure-id.d.ts +2 -0
- package/dist/secure-id.d.ts.map +1 -0
- package/dist/secure-id.js +3 -0
- package/dist/secure-id.js.map +1 -0
- package/dist/types.d.ts +289 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +2 -1
- package/dist/ui.js.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +2 -1
- package/dist/workflow.js.map +1 -1
- package/package.json +1 -1
- package/dist/durable-store-migration.d.ts +0 -26
- package/dist/durable-store-migration.d.ts.map +0 -1
- package/dist/durable-store-migration.js +0 -158
- package/dist/durable-store-migration.js.map +0 -1
package/dist/agent-store.js
CHANGED
|
@@ -1,12 +1,108 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
+
import { normalizeAgentRunState } from "./agent-state.js";
|
|
5
|
+
import { ConflictError, ValidationError } from "./errors.js";
|
|
4
6
|
import { assertPostgresClient } from "./postgres-client.js";
|
|
5
|
-
const cloneState = (state) => JSON.parse(JSON.stringify(state));
|
|
7
|
+
const cloneState = (state) => JSON.parse(JSON.stringify(normalizeAgentRunState(state)));
|
|
6
8
|
const cloneMessages = (messages) => JSON.parse(JSON.stringify(messages));
|
|
7
|
-
const
|
|
9
|
+
const cloneJournalEntry = (entry) => JSON.parse(JSON.stringify(entry));
|
|
10
|
+
const scopePrefix = (scope) => scope
|
|
11
|
+
? `${encodeURIComponent(scope.namespace ?? "default")}:${encodeURIComponent(scope.tenantId)}:${encodeURIComponent(scope.userId ?? "*")}:`
|
|
12
|
+
: "";
|
|
13
|
+
const scopedKey = (scope, value) => `${scopePrefix(scope)}${value}`;
|
|
14
|
+
const defaultMemoryKey = (context) => context.scope
|
|
15
|
+
? scopedKey(context.scope, context.agentId ?? context.runId)
|
|
16
|
+
: context.runId;
|
|
17
|
+
const sameScope = (left, right) => left.tenantId === right.tenantId && left.userId === right.userId && left.namespace === right.namespace;
|
|
18
|
+
const validateScope = (value) => {
|
|
19
|
+
if (!value)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (typeof value.tenantId !== "string" || value.tenantId.length === 0) {
|
|
22
|
+
throw new ValidationError('Agent store scope "tenantId" must be a non-empty string.');
|
|
23
|
+
}
|
|
24
|
+
for (const field of ["userId", "namespace"]) {
|
|
25
|
+
if (value[field] !== undefined && (typeof value[field] !== "string" || value[field].length === 0)) {
|
|
26
|
+
throw new ValidationError(`Agent store scope "${field}" must be a non-empty string when provided.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
const resolveScope = (configured, operation) => {
|
|
32
|
+
configured = validateScope(configured);
|
|
33
|
+
operation = validateScope(operation);
|
|
34
|
+
if (configured && operation && !sameScope(configured, operation)) {
|
|
35
|
+
throw new ValidationError("The operation scope does not match the store scope.");
|
|
36
|
+
}
|
|
37
|
+
return operation ?? configured;
|
|
38
|
+
};
|
|
8
39
|
const fileNameForAgentStoreKey = (key) => `${encodeURIComponent(key)}.json`;
|
|
40
|
+
const fileNameForIdempotencyKey = (key) => `.idempotency-${createHash("sha256").update(key).digest("hex")}.json`;
|
|
9
41
|
const identifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
42
|
+
const normalizeLimit = (value, fallback = 50) => {
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
return fallback;
|
|
45
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 1_000) {
|
|
46
|
+
throw new ValidationError('The "limit" option must be an integer between 1 and 1000.');
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
const validateLeaseOptions = (options) => {
|
|
51
|
+
if (!options.ownerId.trim())
|
|
52
|
+
throw new ValidationError('The lease "ownerId" must not be empty.');
|
|
53
|
+
if (!Number.isSafeInteger(options.ttlMs) || options.ttlMs < 1 || options.ttlMs > 86_400_000) {
|
|
54
|
+
throw new ValidationError('The lease "ttlMs" must be an integer between 1 and 86400000.');
|
|
55
|
+
}
|
|
56
|
+
if (options.now !== undefined && (!Number.isSafeInteger(options.now) || options.now < 0)) {
|
|
57
|
+
throw new ValidationError('The lease "now" value must be a non-negative integer.');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const encodeCursor = (state) => Buffer.from(JSON.stringify([state.updatedAt ?? state.startedAt ?? 0, state.runId]), "utf8").toString("base64url");
|
|
61
|
+
const decodeCursor = (cursor) => {
|
|
62
|
+
if (!cursor)
|
|
63
|
+
return undefined;
|
|
64
|
+
try {
|
|
65
|
+
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
66
|
+
if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== "number" || typeof value[1] !== "string") {
|
|
67
|
+
throw new Error("invalid");
|
|
68
|
+
}
|
|
69
|
+
return [value[0], value[1]];
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new ValidationError('The "cursor" option is invalid.');
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const listStates = (states, options = {}) => {
|
|
76
|
+
const limit = normalizeLimit(options.limit);
|
|
77
|
+
const cursor = decodeCursor(options.cursor);
|
|
78
|
+
const filtered = [...states]
|
|
79
|
+
.filter((state) => options.agentId === undefined || state.agentId === options.agentId)
|
|
80
|
+
.filter((state) => options.parentRunId === undefined || state.parentRunId === options.parentRunId)
|
|
81
|
+
.filter((state) => !options.statuses?.length || options.statuses.includes(state.status))
|
|
82
|
+
.filter((state) => options.updatedAfter === undefined || (state.updatedAt ?? 0) > options.updatedAfter)
|
|
83
|
+
.filter((state) => options.updatedBefore === undefined || (state.updatedAt ?? 0) < options.updatedBefore)
|
|
84
|
+
.sort((left, right) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0) || right.runId.localeCompare(left.runId))
|
|
85
|
+
.filter((state) => !cursor || (state.updatedAt ?? state.startedAt ?? 0) < cursor[0] || ((state.updatedAt ?? state.startedAt ?? 0) === cursor[0] && state.runId < cursor[1]));
|
|
86
|
+
const page = filtered.slice(0, limit);
|
|
87
|
+
return {
|
|
88
|
+
items: page.map(cloneState),
|
|
89
|
+
...(filtered.length > limit && page.at(-1) ? { nextCursor: encodeCursor(page.at(-1)) } : {})
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
const assertJournalRevision = (current, expectedRevision) => {
|
|
93
|
+
if (expectedRevision !== undefined && (!current || current.revision !== expectedRevision)) {
|
|
94
|
+
throw new ConflictError("Agent tool-call journal revision conflict.");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const nextJournalEntry = (entry, options) => {
|
|
98
|
+
if (!entry.runId || !entry.toolCallId || !entry.toolName || !entry.idempotencyKey) {
|
|
99
|
+
throw new ValidationError("Tool-call journal entries require runId, toolCallId, toolName, and idempotencyKey.");
|
|
100
|
+
}
|
|
101
|
+
return cloneJournalEntry({
|
|
102
|
+
...entry,
|
|
103
|
+
revision: options?.expectedRevision === undefined ? entry.revision : options.expectedRevision + 1
|
|
104
|
+
});
|
|
105
|
+
};
|
|
10
106
|
const defaultMemoryMessages = (state) => {
|
|
11
107
|
const lastAssistantMessage = [...state.messages].reverse().find((message) => message.role === "assistant");
|
|
12
108
|
return lastAssistantMessage ? [lastAssistantMessage] : [];
|
|
@@ -41,10 +137,17 @@ const prepareSqliteStatement = (db, sql) => {
|
|
|
41
137
|
const initializeSqliteTable = (db, sql) => {
|
|
42
138
|
db.exec(sql);
|
|
43
139
|
};
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
140
|
+
const assertExpectedRevision = (current, expectedRevision) => {
|
|
141
|
+
if (expectedRevision !== undefined && (current?.revision ?? 0) !== expectedRevision) {
|
|
142
|
+
throw new ConflictError("AgentRunState revision conflict.");
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const nextStoredState = (state, options) => {
|
|
146
|
+
const normalized = normalizeAgentRunState(state);
|
|
147
|
+
return options?.expectedRevision === undefined
|
|
148
|
+
? normalized
|
|
149
|
+
: { ...normalized, revision: options.expectedRevision + 1 };
|
|
150
|
+
};
|
|
48
151
|
const ensurePostgresTable = (() => {
|
|
49
152
|
const initializedTables = new WeakMap();
|
|
50
153
|
return async (client, tableName, createSql) => {
|
|
@@ -61,67 +164,193 @@ const ensurePostgresTable = (() => {
|
|
|
61
164
|
await initialization;
|
|
62
165
|
};
|
|
63
166
|
})();
|
|
64
|
-
export const createInMemoryAgentRunStore = () => {
|
|
167
|
+
export const createInMemoryAgentRunStore = (options = {}) => {
|
|
65
168
|
const states = new Map();
|
|
66
169
|
const idempotencyKeys = new Map();
|
|
67
170
|
const parentRunIds = new Map();
|
|
171
|
+
const leases = new Map();
|
|
172
|
+
const journal = new Map();
|
|
173
|
+
const runKey = (runId, scope) => scopedKey(resolveScope(options.scope, scope), runId);
|
|
174
|
+
const idempotencyKey = (key, scope) => scopedKey(resolveScope(options.scope, scope), key);
|
|
175
|
+
const journalKey = (runId, toolCallId, scope) => `${runKey(runId, scope)}:${toolCallId}`;
|
|
68
176
|
const removeParentIndex = (state) => {
|
|
69
177
|
if (!state?.parentRunId) {
|
|
70
178
|
return;
|
|
71
179
|
}
|
|
72
|
-
const
|
|
73
|
-
children
|
|
180
|
+
const parentKey = scopedKey(resolveScope(options.scope, state.scope), state.parentRunId);
|
|
181
|
+
const children = parentRunIds.get(parentKey);
|
|
182
|
+
children?.delete(runKey(state.runId, state.scope));
|
|
74
183
|
if (children?.size === 0) {
|
|
75
|
-
parentRunIds.delete(
|
|
184
|
+
parentRunIds.delete(parentKey);
|
|
76
185
|
}
|
|
77
186
|
};
|
|
78
187
|
return {
|
|
79
|
-
load(runId) {
|
|
80
|
-
const state = states.get(runId);
|
|
81
|
-
return state ? cloneState(
|
|
188
|
+
load(runId, scope) {
|
|
189
|
+
const state = states.get(runKey(runId, scope));
|
|
190
|
+
return state ? cloneState(normalizeAgentRunState(state)) : undefined;
|
|
82
191
|
},
|
|
83
|
-
findByIdempotencyKey(
|
|
84
|
-
const runId = idempotencyKeys.get(idempotencyKey);
|
|
192
|
+
findByIdempotencyKey(idempotencyKeyValue, scope) {
|
|
193
|
+
const runId = idempotencyKeys.get(idempotencyKey(idempotencyKeyValue, scope));
|
|
85
194
|
if (!runId) {
|
|
86
195
|
return undefined;
|
|
87
196
|
}
|
|
88
197
|
const state = states.get(runId);
|
|
89
|
-
return state ? cloneState(
|
|
198
|
+
return state ? cloneState(normalizeAgentRunState(state)) : undefined;
|
|
90
199
|
},
|
|
91
|
-
findByParentRunId(parentRunId) {
|
|
92
|
-
return [...(parentRunIds.get(parentRunId) ?? [])].flatMap((runId) => {
|
|
200
|
+
findByParentRunId(parentRunId, scope) {
|
|
201
|
+
return [...(parentRunIds.get(scopedKey(resolveScope(options.scope, scope), parentRunId)) ?? [])].flatMap((runId) => {
|
|
93
202
|
const state = states.get(runId);
|
|
94
|
-
return state ? [cloneState(
|
|
203
|
+
return state ? [cloneState(normalizeAgentRunState(state))] : [];
|
|
95
204
|
});
|
|
96
205
|
},
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
states.
|
|
206
|
+
claimIdempotencyKey(state) {
|
|
207
|
+
const scope = resolveScope(options.scope, state.scope);
|
|
208
|
+
const existingRunId = idempotencyKeys.get(idempotencyKey(state.idempotencyKey, scope));
|
|
209
|
+
const existing = existingRunId ? states.get(existingRunId) : undefined;
|
|
210
|
+
if (existing) {
|
|
211
|
+
return { claimed: false, state: cloneState(existing) };
|
|
212
|
+
}
|
|
213
|
+
const normalized = normalizeAgentRunState(state);
|
|
214
|
+
removeParentIndex(states.get(runKey(normalized.runId, scope)));
|
|
215
|
+
const stored = cloneState({ ...normalized, ...(scope ? { scope } : {}) });
|
|
216
|
+
states.set(runKey(normalized.runId, scope), stored);
|
|
217
|
+
idempotencyKeys.set(idempotencyKey(state.idempotencyKey, scope), runKey(normalized.runId, scope));
|
|
218
|
+
if (normalized.parentRunId) {
|
|
219
|
+
const parentKey = scopedKey(scope, normalized.parentRunId);
|
|
220
|
+
const children = parentRunIds.get(parentKey) ?? new Set();
|
|
221
|
+
children.add(runKey(normalized.runId, scope));
|
|
222
|
+
parentRunIds.set(parentKey, children);
|
|
223
|
+
}
|
|
224
|
+
return { claimed: true, state: cloneState(stored) };
|
|
225
|
+
},
|
|
226
|
+
save(state, saveOptions) {
|
|
227
|
+
const scope = resolveScope(options.scope, state.scope);
|
|
228
|
+
const current = states.get(runKey(state.runId, scope));
|
|
229
|
+
assertExpectedRevision(current, saveOptions?.expectedRevision);
|
|
230
|
+
const normalized = nextStoredState(state, saveOptions);
|
|
231
|
+
removeParentIndex(current);
|
|
232
|
+
states.set(runKey(normalized.runId, scope), cloneState({ ...normalized, ...(scope ? { scope } : {}) }));
|
|
101
233
|
if (normalized.idempotencyKey) {
|
|
102
|
-
idempotencyKeys.
|
|
234
|
+
const owner = idempotencyKeys.get(idempotencyKey(normalized.idempotencyKey, scope));
|
|
235
|
+
if (owner && owner !== runKey(normalized.runId, scope)) {
|
|
236
|
+
throw new ConflictError("AgentRunState idempotency key conflict.");
|
|
237
|
+
}
|
|
238
|
+
idempotencyKeys.set(idempotencyKey(normalized.idempotencyKey, scope), runKey(normalized.runId, scope));
|
|
103
239
|
}
|
|
104
240
|
if (normalized.parentRunId) {
|
|
105
|
-
const
|
|
106
|
-
children.
|
|
107
|
-
|
|
241
|
+
const parentKey = scopedKey(scope, normalized.parentRunId);
|
|
242
|
+
const children = parentRunIds.get(parentKey) ?? new Set();
|
|
243
|
+
children.add(runKey(normalized.runId, scope));
|
|
244
|
+
parentRunIds.set(parentKey, children);
|
|
108
245
|
}
|
|
109
246
|
},
|
|
110
|
-
delete(runId) {
|
|
111
|
-
const
|
|
247
|
+
delete(runId, scope) {
|
|
248
|
+
const key = runKey(runId, scope);
|
|
249
|
+
const state = states.get(key);
|
|
112
250
|
if (state?.idempotencyKey) {
|
|
113
|
-
idempotencyKeys.delete(state.idempotencyKey);
|
|
251
|
+
idempotencyKeys.delete(idempotencyKey(state.idempotencyKey, state.scope));
|
|
114
252
|
}
|
|
115
253
|
removeParentIndex(state);
|
|
116
|
-
states.delete(
|
|
254
|
+
states.delete(key);
|
|
255
|
+
leases.delete(key);
|
|
256
|
+
for (const journalEntryKey of journal.keys()) {
|
|
257
|
+
if (journalEntryKey.startsWith(`${key}:`))
|
|
258
|
+
journal.delete(journalEntryKey);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
list(listOptions, scope) {
|
|
262
|
+
const prefix = scopePrefix(resolveScope(options.scope, scope));
|
|
263
|
+
return listStates([...states.entries()].filter(([key]) => key.startsWith(prefix)).map(([, state]) => state), listOptions);
|
|
264
|
+
},
|
|
265
|
+
deleteExpired(retention, scope) {
|
|
266
|
+
const prefix = scopePrefix(resolveScope(options.scope, scope));
|
|
267
|
+
const candidates = listStates([...states.entries()].filter(([key]) => key.startsWith(prefix)).map(([, state]) => state), {
|
|
268
|
+
statuses: retention.statuses,
|
|
269
|
+
updatedBefore: retention.before,
|
|
270
|
+
limit: retention.limit ?? 1_000
|
|
271
|
+
}).items;
|
|
272
|
+
for (const state of candidates)
|
|
273
|
+
this.delete?.(state.runId, state.scope);
|
|
274
|
+
return candidates.length;
|
|
275
|
+
},
|
|
276
|
+
acquireLease(runId, leaseOptions, scope) {
|
|
277
|
+
validateLeaseOptions(leaseOptions);
|
|
278
|
+
const key = runKey(runId, scope);
|
|
279
|
+
if (!states.has(key))
|
|
280
|
+
return undefined;
|
|
281
|
+
const now = leaseOptions.now ?? Date.now();
|
|
282
|
+
const current = leases.get(key);
|
|
283
|
+
if (current && current.expiresAt > now && current.ownerId !== leaseOptions.ownerId)
|
|
284
|
+
return undefined;
|
|
285
|
+
const lease = { runId, ownerId: leaseOptions.ownerId, expiresAt: now + leaseOptions.ttlMs };
|
|
286
|
+
leases.set(key, lease);
|
|
287
|
+
return { ...lease };
|
|
288
|
+
},
|
|
289
|
+
renewLease(runId, leaseOptions, scope) {
|
|
290
|
+
validateLeaseOptions(leaseOptions);
|
|
291
|
+
const key = runKey(runId, scope);
|
|
292
|
+
const now = leaseOptions.now ?? Date.now();
|
|
293
|
+
const current = leases.get(key);
|
|
294
|
+
if (!current || current.ownerId !== leaseOptions.ownerId || current.expiresAt <= now)
|
|
295
|
+
return undefined;
|
|
296
|
+
const lease = { runId, ownerId: leaseOptions.ownerId, expiresAt: now + leaseOptions.ttlMs };
|
|
297
|
+
leases.set(key, lease);
|
|
298
|
+
return { ...lease };
|
|
299
|
+
},
|
|
300
|
+
releaseLease(runId, ownerId, scope) {
|
|
301
|
+
const key = runKey(runId, scope);
|
|
302
|
+
if (leases.get(key)?.ownerId !== ownerId)
|
|
303
|
+
return false;
|
|
304
|
+
return leases.delete(key);
|
|
305
|
+
},
|
|
306
|
+
loadToolCall(runId, toolCallId, scope) {
|
|
307
|
+
const entry = journal.get(journalKey(runId, toolCallId, scope));
|
|
308
|
+
return entry ? cloneJournalEntry(entry) : undefined;
|
|
309
|
+
},
|
|
310
|
+
loadToolExecution(runId, toolCallId, scope) {
|
|
311
|
+
return this.loadToolCall?.(runId, toolCallId, scope);
|
|
312
|
+
},
|
|
313
|
+
listToolCalls(runId, scope) {
|
|
314
|
+
const prefix = `${runKey(runId, scope)}:`;
|
|
315
|
+
return [...journal.entries()]
|
|
316
|
+
.filter(([key]) => key.startsWith(prefix))
|
|
317
|
+
.map(([, entry]) => cloneJournalEntry(entry))
|
|
318
|
+
.sort((left, right) => left.updatedAt - right.updatedAt || left.toolCallId.localeCompare(right.toolCallId));
|
|
319
|
+
},
|
|
320
|
+
saveToolCall(entry, journalOptions) {
|
|
321
|
+
const scope = resolveScope(options.scope, entry.scope);
|
|
322
|
+
if (!states.has(runKey(entry.runId, scope)))
|
|
323
|
+
throw new ValidationError("Cannot journal a tool call for an unknown run.");
|
|
324
|
+
const key = journalKey(entry.runId, entry.toolCallId, scope);
|
|
325
|
+
const current = journal.get(key);
|
|
326
|
+
assertJournalRevision(current, journalOptions?.expectedRevision);
|
|
327
|
+
const next = nextJournalEntry(entry, journalOptions);
|
|
328
|
+
journal.set(key, next);
|
|
329
|
+
return cloneJournalEntry(next);
|
|
330
|
+
},
|
|
331
|
+
claimToolExecution(entry) {
|
|
332
|
+
const key = journalKey(entry.runId, entry.toolCallId, entry.scope);
|
|
333
|
+
const existing = journal.get(key);
|
|
334
|
+
if (existing)
|
|
335
|
+
return { claimed: false, entry: cloneJournalEntry(existing) };
|
|
336
|
+
const claimed = this.saveToolCall?.({ ...entry, status: "running", revision: 0 });
|
|
337
|
+
return { claimed: true, entry: claimed };
|
|
338
|
+
},
|
|
339
|
+
completeToolExecution(entry, journalOptions) {
|
|
340
|
+
return this.saveToolCall?.({ ...entry, status: entry.status === "failed" ? "failed" : "completed" }, journalOptions);
|
|
117
341
|
}
|
|
118
342
|
};
|
|
119
343
|
};
|
|
120
|
-
export const createFileAgentRunStore = (options) =>
|
|
121
|
-
|
|
344
|
+
export const createFileAgentRunStore = (options) => {
|
|
345
|
+
const effectiveScope = (scope) => resolveScope(options.scope, scope);
|
|
346
|
+
const runPath = (runId, scope) => path.join(options.directory, fileNameForAgentStoreKey(scopedKey(effectiveScope(scope), runId)));
|
|
347
|
+
const idempotencyPath = (key, scope) => path.join(options.directory, fileNameForIdempotencyKey(scopedKey(effectiveScope(scope), key)));
|
|
348
|
+
const leasePath = (runId, scope) => path.join(options.directory, `.lease-${createHash("sha256").update(scopedKey(effectiveScope(scope), runId)).digest("hex")}.json`);
|
|
349
|
+
const toolPath = (runId, toolCallId, scope) => path.join(options.directory, `.tool-${createHash("sha256").update(`${scopedKey(effectiveScope(scope), runId)}:${toolCallId}`).digest("hex")}.json`);
|
|
350
|
+
const load = async (runId, scope) => {
|
|
122
351
|
try {
|
|
123
|
-
const content = await fs.readFile(
|
|
124
|
-
return
|
|
352
|
+
const content = await fs.readFile(runPath(runId, scope), "utf8");
|
|
353
|
+
return normalizeAgentRunState(JSON.parse(content));
|
|
125
354
|
}
|
|
126
355
|
catch (error) {
|
|
127
356
|
if (error.code === "ENOENT") {
|
|
@@ -129,76 +358,303 @@ export const createFileAgentRunStore = (options) => ({
|
|
|
129
358
|
}
|
|
130
359
|
throw error;
|
|
131
360
|
}
|
|
132
|
-
}
|
|
133
|
-
async
|
|
134
|
-
|
|
361
|
+
};
|
|
362
|
+
const findByIdempotencyKey = async (idempotencyKey, scope) => {
|
|
363
|
+
const targetScope = effectiveScope(scope);
|
|
135
364
|
try {
|
|
136
|
-
|
|
365
|
+
const marker = await fs.readFile(idempotencyPath(idempotencyKey, scope), "utf8");
|
|
366
|
+
const claimed = normalizeAgentRunState(JSON.parse(marker));
|
|
367
|
+
return (await load(claimed.runId, scope)) ?? claimed;
|
|
137
368
|
}
|
|
138
369
|
catch (error) {
|
|
139
|
-
if (error.code
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
for (const entry of entries) {
|
|
145
|
-
if (!entry.endsWith(".json")) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const content = await fs.readFile(path.join(options.directory, entry), "utf8");
|
|
149
|
-
const state = normalizeRunState(JSON.parse(content));
|
|
150
|
-
if (state.idempotencyKey === idempotencyKey) {
|
|
151
|
-
return state;
|
|
370
|
+
if (error.code !== "ENOENT") {
|
|
371
|
+
throw error;
|
|
152
372
|
}
|
|
153
373
|
}
|
|
154
|
-
return undefined;
|
|
155
|
-
},
|
|
156
|
-
async findByParentRunId(parentRunId) {
|
|
157
374
|
let entries;
|
|
158
375
|
try {
|
|
159
376
|
entries = await fs.readdir(options.directory);
|
|
160
377
|
}
|
|
161
378
|
catch (error) {
|
|
162
|
-
|
|
163
|
-
return [];
|
|
164
|
-
}
|
|
165
|
-
throw error;
|
|
379
|
+
return error.code === "ENOENT" ? undefined : Promise.reject(error);
|
|
166
380
|
}
|
|
167
|
-
const states = [];
|
|
168
381
|
for (const entry of entries) {
|
|
169
|
-
if (!entry.endsWith(".json")) {
|
|
382
|
+
if (!entry.endsWith(".json") || entry.startsWith(".")) {
|
|
170
383
|
continue;
|
|
171
384
|
}
|
|
172
385
|
const content = await fs.readFile(path.join(options.directory, entry), "utf8");
|
|
173
|
-
const state =
|
|
174
|
-
if (state.
|
|
175
|
-
|
|
386
|
+
const state = normalizeAgentRunState(JSON.parse(content));
|
|
387
|
+
if (state.idempotencyKey === idempotencyKey && (!targetScope || (state.scope && sameScope(state.scope, targetScope)))) {
|
|
388
|
+
return state;
|
|
176
389
|
}
|
|
177
390
|
}
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
391
|
+
return undefined;
|
|
392
|
+
};
|
|
393
|
+
return {
|
|
394
|
+
load,
|
|
395
|
+
findByIdempotencyKey,
|
|
396
|
+
async findByParentRunId(parentRunId, scope) {
|
|
397
|
+
const targetScope = effectiveScope(scope);
|
|
398
|
+
let entries;
|
|
399
|
+
try {
|
|
400
|
+
entries = await fs.readdir(options.directory);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
if (error.code === "ENOENT") {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
190
406
|
throw error;
|
|
191
407
|
}
|
|
408
|
+
const states = [];
|
|
409
|
+
for (const entry of entries) {
|
|
410
|
+
if (!entry.endsWith(".json") || entry.startsWith(".")) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const content = await fs.readFile(path.join(options.directory, entry), "utf8");
|
|
414
|
+
const state = normalizeAgentRunState(JSON.parse(content));
|
|
415
|
+
if (state.parentRunId === parentRunId && (!targetScope || (state.scope && sameScope(state.scope, targetScope)))) {
|
|
416
|
+
states.push(state);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return states;
|
|
420
|
+
},
|
|
421
|
+
async claimIdempotencyKey(state) {
|
|
422
|
+
await fs.mkdir(options.directory, { recursive: true });
|
|
423
|
+
const scope = effectiveScope(state.scope);
|
|
424
|
+
const normalized = normalizeAgentRunState({ ...state, ...(scope ? { scope } : {}) });
|
|
425
|
+
try {
|
|
426
|
+
await fs.writeFile(idempotencyPath(state.idempotencyKey, scope), JSON.stringify(normalized, null, 2), {
|
|
427
|
+
encoding: "utf8",
|
|
428
|
+
flag: "wx"
|
|
429
|
+
});
|
|
430
|
+
await fs.writeFile(runPath(normalized.runId, scope), JSON.stringify(normalized, null, 2), "utf8");
|
|
431
|
+
return { claimed: true, state: normalized };
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
if (error.code !== "EEXIST") {
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
const existing = await findByIdempotencyKey(state.idempotencyKey, scope);
|
|
438
|
+
if (!existing) {
|
|
439
|
+
throw new ConflictError("AgentRunState idempotency claim could not be loaded.");
|
|
440
|
+
}
|
|
441
|
+
return { claimed: false, state: existing };
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
async save(state, saveOptions) {
|
|
445
|
+
await fs.mkdir(options.directory, { recursive: true });
|
|
446
|
+
const scope = effectiveScope(state.scope);
|
|
447
|
+
const current = await load(state.runId, scope);
|
|
448
|
+
assertExpectedRevision(current, saveOptions?.expectedRevision);
|
|
449
|
+
const normalized = nextStoredState(state, saveOptions);
|
|
450
|
+
if (normalized.idempotencyKey) {
|
|
451
|
+
const owner = await findByIdempotencyKey(normalized.idempotencyKey, scope);
|
|
452
|
+
if (owner && owner.runId !== normalized.runId) {
|
|
453
|
+
throw new ConflictError("AgentRunState idempotency key conflict.");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const stored = { ...normalized, ...(scope ? { scope } : {}) };
|
|
457
|
+
await fs.writeFile(runPath(normalized.runId, scope), JSON.stringify(stored, null, 2), "utf8");
|
|
458
|
+
if (normalized.idempotencyKey) {
|
|
459
|
+
await fs.writeFile(idempotencyPath(normalized.idempotencyKey, scope), JSON.stringify(stored, null, 2), "utf8");
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
async delete(runId, scope) {
|
|
463
|
+
const current = await load(runId, scope);
|
|
464
|
+
const targetScope = effectiveScope(scope);
|
|
465
|
+
try {
|
|
466
|
+
await fs.unlink(runPath(runId, scope));
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
if (error.code !== "ENOENT") {
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (current?.idempotencyKey) {
|
|
474
|
+
try {
|
|
475
|
+
await fs.unlink(idempotencyPath(current.idempotencyKey, scope));
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
if (error.code !== "ENOENT") {
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
await fs.unlink(leasePath(runId, scope)).catch((error) => {
|
|
484
|
+
if (error.code !== "ENOENT")
|
|
485
|
+
throw error;
|
|
486
|
+
});
|
|
487
|
+
const entries = await fs.readdir(options.directory).catch((error) => {
|
|
488
|
+
if (error.code === "ENOENT")
|
|
489
|
+
return [];
|
|
490
|
+
throw error;
|
|
491
|
+
});
|
|
492
|
+
for (const entryName of entries.filter((name) => name.startsWith(".tool-") && name.endsWith(".json"))) {
|
|
493
|
+
const entryPath = path.join(options.directory, entryName);
|
|
494
|
+
try {
|
|
495
|
+
const journalEntry = JSON.parse(await fs.readFile(entryPath, "utf8"));
|
|
496
|
+
const sameEntryScope = targetScope
|
|
497
|
+
? Boolean(journalEntry.scope && sameScope(journalEntry.scope, targetScope))
|
|
498
|
+
: journalEntry.scope === undefined;
|
|
499
|
+
if (journalEntry.runId === runId && sameEntryScope) {
|
|
500
|
+
await fs.unlink(entryPath);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
if (error.code !== "ENOENT")
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
async list(listOptions, scope) {
|
|
510
|
+
const targetScope = effectiveScope(scope);
|
|
511
|
+
const entries = await fs.readdir(options.directory).catch((error) => {
|
|
512
|
+
if (error.code === "ENOENT")
|
|
513
|
+
return [];
|
|
514
|
+
throw error;
|
|
515
|
+
});
|
|
516
|
+
const states = [];
|
|
517
|
+
for (const entry of entries) {
|
|
518
|
+
if (!entry.endsWith(".json") || entry.startsWith("."))
|
|
519
|
+
continue;
|
|
520
|
+
const state = normalizeAgentRunState(JSON.parse(await fs.readFile(path.join(options.directory, entry), "utf8")));
|
|
521
|
+
if (!targetScope || (state.scope && sameScope(state.scope, targetScope)))
|
|
522
|
+
states.push(state);
|
|
523
|
+
}
|
|
524
|
+
return listStates(states, listOptions);
|
|
525
|
+
},
|
|
526
|
+
async deleteExpired(retention, scope) {
|
|
527
|
+
const page = await this.list?.({ statuses: retention.statuses, updatedBefore: retention.before, limit: retention.limit ?? 1_000 }, scope);
|
|
528
|
+
const items = page.items;
|
|
529
|
+
for (const state of items)
|
|
530
|
+
await this.delete?.(state.runId, state.scope);
|
|
531
|
+
return items.length;
|
|
532
|
+
},
|
|
533
|
+
async acquireLease(runId, leaseOptions, scope) {
|
|
534
|
+
validateLeaseOptions(leaseOptions);
|
|
535
|
+
if (!await load(runId, scope))
|
|
536
|
+
return undefined;
|
|
537
|
+
await fs.mkdir(options.directory, { recursive: true });
|
|
538
|
+
const file = leasePath(runId, scope);
|
|
539
|
+
const now = leaseOptions.now ?? Date.now();
|
|
540
|
+
const lease = { runId, ownerId: leaseOptions.ownerId, expiresAt: now + leaseOptions.ttlMs };
|
|
541
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
542
|
+
try {
|
|
543
|
+
await fs.writeFile(file, JSON.stringify(lease), { encoding: "utf8", flag: "wx" });
|
|
544
|
+
return lease;
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
if (error.code !== "EEXIST")
|
|
548
|
+
throw error;
|
|
549
|
+
const current = JSON.parse(await fs.readFile(file, "utf8"));
|
|
550
|
+
if (current.ownerId === leaseOptions.ownerId) {
|
|
551
|
+
await fs.writeFile(file, JSON.stringify(lease), "utf8");
|
|
552
|
+
return lease;
|
|
553
|
+
}
|
|
554
|
+
if (current.expiresAt > now)
|
|
555
|
+
return undefined;
|
|
556
|
+
await fs.unlink(file).catch(() => undefined);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return undefined;
|
|
560
|
+
},
|
|
561
|
+
async renewLease(runId, leaseOptions, scope) {
|
|
562
|
+
validateLeaseOptions(leaseOptions);
|
|
563
|
+
const file = leasePath(runId, scope);
|
|
564
|
+
const now = leaseOptions.now ?? Date.now();
|
|
565
|
+
try {
|
|
566
|
+
const current = JSON.parse(await fs.readFile(file, "utf8"));
|
|
567
|
+
if (current.ownerId !== leaseOptions.ownerId || current.expiresAt <= now)
|
|
568
|
+
return undefined;
|
|
569
|
+
const lease = { runId, ownerId: leaseOptions.ownerId, expiresAt: now + leaseOptions.ttlMs };
|
|
570
|
+
await fs.writeFile(file, JSON.stringify(lease), "utf8");
|
|
571
|
+
return lease;
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
if (error.code === "ENOENT")
|
|
575
|
+
return undefined;
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
async releaseLease(runId, ownerId, scope) {
|
|
580
|
+
const file = leasePath(runId, scope);
|
|
581
|
+
try {
|
|
582
|
+
const current = JSON.parse(await fs.readFile(file, "utf8"));
|
|
583
|
+
if (current.ownerId !== ownerId)
|
|
584
|
+
return false;
|
|
585
|
+
await fs.unlink(file);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
if (error.code === "ENOENT")
|
|
590
|
+
return false;
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
async loadToolCall(runId, toolCallId, scope) {
|
|
595
|
+
try {
|
|
596
|
+
return JSON.parse(await fs.readFile(toolPath(runId, toolCallId, scope), "utf8"));
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
if (error.code === "ENOENT")
|
|
600
|
+
return undefined;
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
async loadToolExecution(runId, toolCallId, scope) {
|
|
605
|
+
return this.loadToolCall?.(runId, toolCallId, scope);
|
|
606
|
+
},
|
|
607
|
+
async listToolCalls(runId, scope) {
|
|
608
|
+
const targetScope = effectiveScope(scope);
|
|
609
|
+
const entries = await fs.readdir(options.directory).catch(() => []);
|
|
610
|
+
const results = [];
|
|
611
|
+
for (const entry of entries) {
|
|
612
|
+
if (!entry.startsWith(".tool-") || !entry.endsWith(".json"))
|
|
613
|
+
continue;
|
|
614
|
+
const value = JSON.parse(await fs.readFile(path.join(options.directory, entry), "utf8"));
|
|
615
|
+
if (value.runId === runId && (!targetScope || (value.scope && sameScope(value.scope, targetScope))))
|
|
616
|
+
results.push(value);
|
|
617
|
+
}
|
|
618
|
+
return results.sort((a, b) => a.updatedAt - b.updatedAt || a.toolCallId.localeCompare(b.toolCallId));
|
|
619
|
+
},
|
|
620
|
+
async saveToolCall(entry, journalOptions) {
|
|
621
|
+
const file = toolPath(entry.runId, entry.toolCallId, entry.scope);
|
|
622
|
+
const current = await this.loadToolCall?.(entry.runId, entry.toolCallId, entry.scope);
|
|
623
|
+
assertJournalRevision(current, journalOptions?.expectedRevision);
|
|
624
|
+
const next = nextJournalEntry(entry, journalOptions);
|
|
625
|
+
await fs.writeFile(file, JSON.stringify(next, null, 2), "utf8");
|
|
626
|
+
return next;
|
|
627
|
+
},
|
|
628
|
+
async claimToolExecution(entry) {
|
|
629
|
+
await fs.mkdir(options.directory, { recursive: true });
|
|
630
|
+
if (!await load(entry.runId, entry.scope))
|
|
631
|
+
throw new ValidationError("Cannot journal a tool call for an unknown run.");
|
|
632
|
+
const next = nextJournalEntry({ ...entry, status: "running", revision: 0 });
|
|
633
|
+
try {
|
|
634
|
+
await fs.writeFile(toolPath(entry.runId, entry.toolCallId, entry.scope), JSON.stringify(next, null, 2), { encoding: "utf8", flag: "wx" });
|
|
635
|
+
return { claimed: true, entry: next };
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
if (error.code !== "EEXIST")
|
|
639
|
+
throw error;
|
|
640
|
+
const existing = await this.loadToolCall?.(entry.runId, entry.toolCallId, entry.scope);
|
|
641
|
+
if (!existing)
|
|
642
|
+
throw new ConflictError("Tool execution claim could not be loaded.");
|
|
643
|
+
return { claimed: false, entry: existing };
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
async completeToolExecution(entry, journalOptions) {
|
|
647
|
+
return this.saveToolCall?.({ ...entry, status: entry.status === "failed" ? "failed" : "completed" }, journalOptions);
|
|
192
648
|
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
649
|
+
};
|
|
650
|
+
};
|
|
195
651
|
export const createInMemoryAgentMemoryStore = (options = {}) => {
|
|
196
|
-
const keyFor = options.key ?? defaultMemoryKey;
|
|
652
|
+
const keyFor = (context) => (options.key ?? defaultMemoryKey)({ ...context, scope: resolveScope(options.scope, context.scope) });
|
|
197
653
|
const selectMessages = options.selectMessages ?? defaultMemoryMessages;
|
|
198
654
|
const memories = new Map(Object.entries(options.initialMessages ?? {}).map(([key, messages]) => [key, cloneMessages(messages)]));
|
|
199
655
|
return {
|
|
200
656
|
load(context) {
|
|
201
|
-
return cloneMessages(memories.get(keyFor(context)) ?? []);
|
|
657
|
+
return cloneMessages(memories.get(keyFor(context)) ?? (context.agentId ? options.initialMessages?.[context.agentId] : undefined) ?? []);
|
|
202
658
|
},
|
|
203
659
|
save(context) {
|
|
204
660
|
memories.set(keyFor(context), cloneMessages(selectMessages(context.state)));
|
|
@@ -206,7 +662,7 @@ export const createInMemoryAgentMemoryStore = (options = {}) => {
|
|
|
206
662
|
};
|
|
207
663
|
};
|
|
208
664
|
export const createFileAgentMemoryStore = (options) => {
|
|
209
|
-
const keyFor = options.key ?? defaultMemoryKey;
|
|
665
|
+
const keyFor = (context) => (options.key ?? defaultMemoryKey)({ ...context, scope: resolveScope(options.scope, context.scope) });
|
|
210
666
|
const selectMessages = options.selectMessages ?? defaultMemoryMessages;
|
|
211
667
|
return {
|
|
212
668
|
async load(context) {
|
|
@@ -231,11 +687,31 @@ export const createSqliteAgentRunStore = (options) => {
|
|
|
231
687
|
const tableName = validateIdentifier(options.tableName ?? "zhivex_agent_runs", "tableName");
|
|
232
688
|
const idempotencyTableName = `${tableName}_idempotency`;
|
|
233
689
|
const parentTableName = `${tableName}_parents`;
|
|
690
|
+
const leaseTableName = `${tableName}_leases`;
|
|
691
|
+
const journalTableName = `${tableName}_tool_journal`;
|
|
692
|
+
const dbKey = (value, scope) => scopedKey(resolveScope(options.scope, scope), value);
|
|
234
693
|
initializeSqliteTable(options.db, `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
235
694
|
run_id TEXT PRIMARY KEY,
|
|
236
695
|
state_json TEXT NOT NULL,
|
|
237
696
|
updated_at_ms INTEGER NOT NULL
|
|
238
697
|
)`);
|
|
698
|
+
initializeSqliteTable(options.db, `CREATE INDEX IF NOT EXISTS ${tableName}_updated_idx ON ${tableName} (updated_at_ms, run_id)`);
|
|
699
|
+
initializeSqliteTable(options.db, `CREATE TABLE IF NOT EXISTS ${leaseTableName} (
|
|
700
|
+
run_key TEXT PRIMARY KEY,
|
|
701
|
+
run_id TEXT NOT NULL,
|
|
702
|
+
owner_id TEXT NOT NULL,
|
|
703
|
+
expires_at_ms INTEGER NOT NULL
|
|
704
|
+
)`);
|
|
705
|
+
initializeSqliteTable(options.db, `CREATE INDEX IF NOT EXISTS ${leaseTableName}_expiry_idx ON ${leaseTableName} (expires_at_ms)`);
|
|
706
|
+
initializeSqliteTable(options.db, `CREATE TABLE IF NOT EXISTS ${journalTableName} (
|
|
707
|
+
run_key TEXT NOT NULL,
|
|
708
|
+
tool_call_id TEXT NOT NULL,
|
|
709
|
+
entry_json TEXT NOT NULL,
|
|
710
|
+
revision INTEGER NOT NULL,
|
|
711
|
+
updated_at_ms INTEGER NOT NULL,
|
|
712
|
+
PRIMARY KEY (run_key, tool_call_id)
|
|
713
|
+
)`);
|
|
714
|
+
initializeSqliteTable(options.db, `CREATE INDEX IF NOT EXISTS ${journalTableName}_run_idx ON ${journalTableName} (run_key, updated_at_ms)`);
|
|
239
715
|
initializeSqliteTable(options.db, `CREATE TABLE IF NOT EXISTS ${idempotencyTableName} (
|
|
240
716
|
idempotency_key TEXT PRIMARY KEY,
|
|
241
717
|
run_id TEXT NOT NULL,
|
|
@@ -246,6 +722,7 @@ export const createSqliteAgentRunStore = (options) => {
|
|
|
246
722
|
parent_run_id TEXT NOT NULL,
|
|
247
723
|
updated_at_ms INTEGER NOT NULL
|
|
248
724
|
)`);
|
|
725
|
+
initializeSqliteTable(options.db, `CREATE INDEX IF NOT EXISTS ${parentTableName}_parent_idx ON ${parentTableName} (parent_run_id, updated_at_ms)`);
|
|
249
726
|
const loadStatement = prepareSqliteStatement(options.db, `SELECT state_json FROM ${tableName} WHERE run_id = ?`);
|
|
250
727
|
const saveStatement = prepareSqliteStatement(options.db, `
|
|
251
728
|
INSERT INTO ${tableName} (run_id, state_json, updated_at_ms)
|
|
@@ -262,9 +739,7 @@ export const createSqliteAgentRunStore = (options) => {
|
|
|
262
739
|
const saveIdempotencyStatement = prepareSqliteStatement(options.db, `
|
|
263
740
|
INSERT INTO ${idempotencyTableName} (idempotency_key, run_id, updated_at_ms)
|
|
264
741
|
VALUES (?, ?, ?)
|
|
265
|
-
ON CONFLICT(idempotency_key) DO
|
|
266
|
-
run_id = excluded.run_id,
|
|
267
|
-
updated_at_ms = excluded.updated_at_ms
|
|
742
|
+
ON CONFLICT(idempotency_key) DO NOTHING
|
|
268
743
|
`);
|
|
269
744
|
const deleteIdempotencyStatement = prepareSqliteStatement(options.db, `DELETE FROM ${idempotencyTableName} WHERE run_id = ?`);
|
|
270
745
|
const findParentStatement = prepareSqliteStatement(options.db, `SELECT runs.state_json
|
|
@@ -279,51 +754,256 @@ export const createSqliteAgentRunStore = (options) => {
|
|
|
279
754
|
updated_at_ms = excluded.updated_at_ms
|
|
280
755
|
`);
|
|
281
756
|
const deleteParentStatement = prepareSqliteStatement(options.db, `DELETE FROM ${parentTableName} WHERE run_id = ?`);
|
|
757
|
+
const listStatement = prepareSqliteStatement(options.db, `SELECT state_json FROM ${tableName}`);
|
|
758
|
+
const loadLeaseStatement = prepareSqliteStatement(options.db, `SELECT owner_id, expires_at_ms FROM ${leaseTableName} WHERE run_key = ?`);
|
|
759
|
+
const saveLeaseStatement = prepareSqliteStatement(options.db, `INSERT INTO ${leaseTableName} (run_key, run_id, owner_id, expires_at_ms) VALUES (?, ?, ?, ?) ON CONFLICT(run_key) DO UPDATE SET owner_id = excluded.owner_id, expires_at_ms = excluded.expires_at_ms`);
|
|
760
|
+
const deleteLeaseStatement = prepareSqliteStatement(options.db, `DELETE FROM ${leaseTableName} WHERE run_key = ? AND owner_id = ?`);
|
|
761
|
+
const deleteRunLeaseStatement = prepareSqliteStatement(options.db, `DELETE FROM ${leaseTableName} WHERE run_key = ?`);
|
|
762
|
+
const deleteRunJournalStatement = prepareSqliteStatement(options.db, `DELETE FROM ${journalTableName} WHERE run_key = ?`);
|
|
763
|
+
const loadJournalStatement = prepareSqliteStatement(options.db, `SELECT entry_json FROM ${journalTableName} WHERE run_key = ? AND tool_call_id = ?`);
|
|
764
|
+
const listJournalStatement = prepareSqliteStatement(options.db, `SELECT entry_json FROM ${journalTableName} WHERE run_key = ? ORDER BY updated_at_ms, tool_call_id`);
|
|
765
|
+
const insertJournalStatement = prepareSqliteStatement(options.db, `INSERT INTO ${journalTableName} (run_key, tool_call_id, entry_json, revision, updated_at_ms) VALUES (?, ?, ?, ?, ?) ON CONFLICT(run_key, tool_call_id) DO NOTHING`);
|
|
766
|
+
const saveJournalStatement = prepareSqliteStatement(options.db, `INSERT INTO ${journalTableName} (run_key, tool_call_id, entry_json, revision, updated_at_ms) VALUES (?, ?, ?, ?, ?) ON CONFLICT(run_key, tool_call_id) DO UPDATE SET entry_json = excluded.entry_json, revision = excluded.revision, updated_at_ms = excluded.updated_at_ms`);
|
|
282
767
|
return {
|
|
283
|
-
load(runId) {
|
|
284
|
-
const row = loadStatement.get([runId]);
|
|
768
|
+
load(runId, scope) {
|
|
769
|
+
const row = loadStatement.get([dbKey(runId, scope)]);
|
|
285
770
|
const stateJson = getRecordField(row, ["state_json", "stateJson"]);
|
|
286
|
-
return typeof stateJson === "string" ?
|
|
771
|
+
return typeof stateJson === "string" ? normalizeAgentRunState(JSON.parse(stateJson)) : undefined;
|
|
287
772
|
},
|
|
288
|
-
findByIdempotencyKey(idempotencyKey) {
|
|
289
|
-
const row = findIdempotencyStatement.get([idempotencyKey]);
|
|
773
|
+
findByIdempotencyKey(idempotencyKey, scope) {
|
|
774
|
+
const row = findIdempotencyStatement.get([dbKey(idempotencyKey, scope)]);
|
|
290
775
|
const stateJson = getRecordField(row, ["state_json", "stateJson"]);
|
|
291
|
-
return typeof stateJson === "string" ?
|
|
776
|
+
return typeof stateJson === "string" ? normalizeAgentRunState(JSON.parse(stateJson)) : undefined;
|
|
292
777
|
},
|
|
293
|
-
findByParentRunId(parentRunId) {
|
|
294
|
-
const rows = findParentStatement.all?.([parentRunId]);
|
|
778
|
+
findByParentRunId(parentRunId, scope) {
|
|
779
|
+
const rows = findParentStatement.all?.([dbKey(parentRunId, scope)]);
|
|
295
780
|
if (Array.isArray(rows)) {
|
|
296
781
|
return rows.flatMap((row) => {
|
|
297
782
|
const stateJson = getRecordField(row, ["state_json", "stateJson"]);
|
|
298
|
-
return typeof stateJson === "string" ? [
|
|
783
|
+
return typeof stateJson === "string" ? [normalizeAgentRunState(JSON.parse(stateJson))] : [];
|
|
299
784
|
});
|
|
300
785
|
}
|
|
301
|
-
const row = findParentStatement.get([parentRunId]);
|
|
786
|
+
const row = findParentStatement.get([dbKey(parentRunId, scope)]);
|
|
302
787
|
const stateJson = getRecordField(row, ["state_json", "stateJson"]);
|
|
303
|
-
return typeof stateJson === "string" ? [
|
|
788
|
+
return typeof stateJson === "string" ? [normalizeAgentRunState(JSON.parse(stateJson))] : [];
|
|
304
789
|
},
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
790
|
+
claimIdempotencyKey(state) {
|
|
791
|
+
options.db.exec("BEGIN IMMEDIATE");
|
|
792
|
+
try {
|
|
793
|
+
const scope = resolveScope(options.scope, state.scope);
|
|
794
|
+
const existingRow = findIdempotencyStatement.get([dbKey(state.idempotencyKey, scope)]);
|
|
795
|
+
const existingJson = getRecordField(existingRow, ["state_json", "stateJson"]);
|
|
796
|
+
if (typeof existingJson === "string") {
|
|
797
|
+
options.db.exec("COMMIT");
|
|
798
|
+
return { claimed: false, state: normalizeAgentRunState(JSON.parse(existingJson)) };
|
|
799
|
+
}
|
|
800
|
+
const normalized = normalizeAgentRunState({ ...state, ...(scope ? { scope } : {}) });
|
|
801
|
+
const updatedAt = Date.now();
|
|
802
|
+
saveStatement.run([dbKey(normalized.runId, scope), JSON.stringify(normalized), updatedAt]);
|
|
803
|
+
saveIdempotencyStatement.run([dbKey(state.idempotencyKey, scope), dbKey(normalized.runId, scope), updatedAt]);
|
|
804
|
+
if (normalized.parentRunId) {
|
|
805
|
+
saveParentStatement.run([dbKey(normalized.runId, scope), dbKey(normalized.parentRunId, scope), updatedAt]);
|
|
806
|
+
}
|
|
807
|
+
options.db.exec("COMMIT");
|
|
808
|
+
return { claimed: true, state: normalized };
|
|
311
809
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
810
|
+
catch (error) {
|
|
811
|
+
options.db.exec("ROLLBACK");
|
|
812
|
+
throw error;
|
|
315
813
|
}
|
|
316
814
|
},
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
815
|
+
save(state, saveOptions) {
|
|
816
|
+
options.db.exec("BEGIN IMMEDIATE");
|
|
817
|
+
try {
|
|
818
|
+
const scope = resolveScope(options.scope, state.scope);
|
|
819
|
+
const currentRow = loadStatement.get([dbKey(state.runId, scope)]);
|
|
820
|
+
const currentJson = getRecordField(currentRow, ["state_json", "stateJson"]);
|
|
821
|
+
const current = typeof currentJson === "string"
|
|
822
|
+
? normalizeAgentRunState(JSON.parse(currentJson))
|
|
823
|
+
: undefined;
|
|
824
|
+
assertExpectedRevision(current, saveOptions?.expectedRevision);
|
|
825
|
+
const normalized = nextStoredState(state, saveOptions);
|
|
826
|
+
if (normalized.idempotencyKey) {
|
|
827
|
+
const ownerRow = findIdempotencyStatement.get([dbKey(normalized.idempotencyKey, scope)]);
|
|
828
|
+
const ownerJson = getRecordField(ownerRow, ["state_json", "stateJson"]);
|
|
829
|
+
const owner = typeof ownerJson === "string"
|
|
830
|
+
? normalizeAgentRunState(JSON.parse(ownerJson))
|
|
831
|
+
: undefined;
|
|
832
|
+
if (owner && owner.runId !== normalized.runId) {
|
|
833
|
+
throw new ConflictError("AgentRunState idempotency key conflict.");
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const updatedAt = Date.now();
|
|
837
|
+
const stored = { ...normalized, ...(scope ? { scope } : {}) };
|
|
838
|
+
saveStatement.run([dbKey(normalized.runId, scope), JSON.stringify(stored), updatedAt]);
|
|
839
|
+
if (normalized.idempotencyKey) {
|
|
840
|
+
saveIdempotencyStatement.run([dbKey(normalized.idempotencyKey, scope), dbKey(normalized.runId, scope), updatedAt]);
|
|
841
|
+
}
|
|
842
|
+
deleteParentStatement.run([dbKey(normalized.runId, scope)]);
|
|
843
|
+
if (normalized.parentRunId) {
|
|
844
|
+
saveParentStatement.run([dbKey(normalized.runId, scope), dbKey(normalized.parentRunId, scope), updatedAt]);
|
|
845
|
+
}
|
|
846
|
+
options.db.exec("COMMIT");
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
options.db.exec("ROLLBACK");
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
delete(runId, scope) {
|
|
854
|
+
const key = dbKey(runId, scope);
|
|
855
|
+
options.db.exec("BEGIN IMMEDIATE");
|
|
856
|
+
try {
|
|
857
|
+
deleteStatement.run([key]);
|
|
858
|
+
deleteIdempotencyStatement.run([key]);
|
|
859
|
+
deleteParentStatement.run([key]);
|
|
860
|
+
deleteRunLeaseStatement.run([key]);
|
|
861
|
+
deleteRunJournalStatement.run([key]);
|
|
862
|
+
options.db.exec("COMMIT");
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
options.db.exec("ROLLBACK");
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
list(listOptions, scope) {
|
|
870
|
+
const rows = listStatement.all?.([]) ?? [];
|
|
871
|
+
const prefix = scopePrefix(resolveScope(options.scope, scope));
|
|
872
|
+
const states = rows.flatMap((row) => {
|
|
873
|
+
const value = getRecordField(row, ["state_json", "stateJson"]);
|
|
874
|
+
if (typeof value !== "string")
|
|
875
|
+
return [];
|
|
876
|
+
const state = normalizeAgentRunState(JSON.parse(value));
|
|
877
|
+
return scopedKey(state.scope, state.runId).startsWith(prefix) ? [state] : [];
|
|
878
|
+
});
|
|
879
|
+
return listStates(states, listOptions);
|
|
880
|
+
},
|
|
881
|
+
deleteExpired(retention, scope) {
|
|
882
|
+
const page = this.list?.({ statuses: retention.statuses, updatedBefore: retention.before, limit: retention.limit ?? 1_000 }, scope);
|
|
883
|
+
for (const state of page.items)
|
|
884
|
+
this.delete?.(state.runId, state.scope);
|
|
885
|
+
return page.items.length;
|
|
886
|
+
},
|
|
887
|
+
acquireLease(runId, leaseOptions, scope) {
|
|
888
|
+
validateLeaseOptions(leaseOptions);
|
|
889
|
+
const now = leaseOptions.now ?? Date.now();
|
|
890
|
+
const key = dbKey(runId, scope);
|
|
891
|
+
options.db.exec("BEGIN IMMEDIATE");
|
|
892
|
+
try {
|
|
893
|
+
const row = loadLeaseStatement.get([key]);
|
|
894
|
+
const owner = getRecordField(row, ["owner_id", "ownerId"]);
|
|
895
|
+
const expiry = getRecordField(row, ["expires_at_ms", "expiresAtMs"]);
|
|
896
|
+
if (typeof owner === "string" && owner !== leaseOptions.ownerId && typeof expiry === "number" && expiry > now) {
|
|
897
|
+
options.db.exec("COMMIT");
|
|
898
|
+
return undefined;
|
|
899
|
+
}
|
|
900
|
+
const lease = { runId, ownerId: leaseOptions.ownerId, expiresAt: now + leaseOptions.ttlMs };
|
|
901
|
+
saveLeaseStatement.run([key, runId, lease.ownerId, lease.expiresAt]);
|
|
902
|
+
options.db.exec("COMMIT");
|
|
903
|
+
return lease;
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
options.db.exec("ROLLBACK");
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
renewLease(runId, leaseOptions, scope) {
|
|
911
|
+
validateLeaseOptions(leaseOptions);
|
|
912
|
+
const now = leaseOptions.now ?? Date.now();
|
|
913
|
+
const key = dbKey(runId, scope);
|
|
914
|
+
options.db.exec("BEGIN IMMEDIATE");
|
|
915
|
+
try {
|
|
916
|
+
const row = loadLeaseStatement.get([key]);
|
|
917
|
+
const owner = getRecordField(row, ["owner_id", "ownerId"]);
|
|
918
|
+
const expiry = getRecordField(row, ["expires_at_ms", "expiresAtMs"]);
|
|
919
|
+
if (owner !== leaseOptions.ownerId || typeof expiry !== "number" || expiry <= now) {
|
|
920
|
+
options.db.exec("COMMIT");
|
|
921
|
+
return undefined;
|
|
922
|
+
}
|
|
923
|
+
const lease = { runId, ownerId: leaseOptions.ownerId, expiresAt: now + leaseOptions.ttlMs };
|
|
924
|
+
saveLeaseStatement.run([key, runId, lease.ownerId, lease.expiresAt]);
|
|
925
|
+
options.db.exec("COMMIT");
|
|
926
|
+
return lease;
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
options.db.exec("ROLLBACK");
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
releaseLease(runId, ownerId, scope) {
|
|
934
|
+
const key = dbKey(runId, scope);
|
|
935
|
+
options.db.exec("BEGIN IMMEDIATE");
|
|
936
|
+
try {
|
|
937
|
+
const row = loadLeaseStatement.get([key]);
|
|
938
|
+
if (getRecordField(row, ["owner_id", "ownerId"]) !== ownerId) {
|
|
939
|
+
options.db.exec("COMMIT");
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
deleteLeaseStatement.run([key, ownerId]);
|
|
943
|
+
options.db.exec("COMMIT");
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
options.db.exec("ROLLBACK");
|
|
948
|
+
throw error;
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
loadToolCall(runId, toolCallId, scope) {
|
|
952
|
+
const row = loadJournalStatement.get([dbKey(runId, scope), toolCallId]);
|
|
953
|
+
const value = getRecordField(row, ["entry_json", "entryJson"]);
|
|
954
|
+
return typeof value === "string" ? JSON.parse(value) : undefined;
|
|
955
|
+
},
|
|
956
|
+
loadToolExecution(runId, toolCallId, scope) {
|
|
957
|
+
return this.loadToolCall?.(runId, toolCallId, scope);
|
|
958
|
+
},
|
|
959
|
+
listToolCalls(runId, scope) {
|
|
960
|
+
const rows = listJournalStatement.all?.([dbKey(runId, scope)]) ?? [];
|
|
961
|
+
return rows.flatMap((row) => {
|
|
962
|
+
const value = getRecordField(row, ["entry_json", "entryJson"]);
|
|
963
|
+
return typeof value === "string" ? [JSON.parse(value)] : [];
|
|
964
|
+
});
|
|
965
|
+
},
|
|
966
|
+
saveToolCall(entry, journalOptions) {
|
|
967
|
+
options.db.exec("BEGIN IMMEDIATE");
|
|
968
|
+
try {
|
|
969
|
+
const current = this.loadToolCall?.(entry.runId, entry.toolCallId, entry.scope);
|
|
970
|
+
assertJournalRevision(current, journalOptions?.expectedRevision);
|
|
971
|
+
const next = nextJournalEntry(entry, journalOptions);
|
|
972
|
+
saveJournalStatement.run([dbKey(entry.runId, entry.scope), entry.toolCallId, JSON.stringify(next), next.revision, next.updatedAt]);
|
|
973
|
+
options.db.exec("COMMIT");
|
|
974
|
+
return next;
|
|
975
|
+
}
|
|
976
|
+
catch (error) {
|
|
977
|
+
options.db.exec("ROLLBACK");
|
|
978
|
+
throw error;
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
claimToolExecution(entry) {
|
|
982
|
+
options.db.exec("BEGIN IMMEDIATE");
|
|
983
|
+
try {
|
|
984
|
+
const current = this.loadToolCall?.(entry.runId, entry.toolCallId, entry.scope);
|
|
985
|
+
if (current) {
|
|
986
|
+
options.db.exec("COMMIT");
|
|
987
|
+
return { claimed: false, entry: current };
|
|
988
|
+
}
|
|
989
|
+
const next = nextJournalEntry({ ...entry, status: "running", revision: 0 });
|
|
990
|
+
insertJournalStatement.run([dbKey(entry.runId, entry.scope), entry.toolCallId, JSON.stringify(next), 0, next.updatedAt]);
|
|
991
|
+
options.db.exec("COMMIT");
|
|
992
|
+
return { claimed: true, entry: next };
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
options.db.exec("ROLLBACK");
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
completeToolExecution(entry, journalOptions) {
|
|
1000
|
+
return this.saveToolCall?.({ ...entry, status: entry.status === "failed" ? "failed" : "completed" }, journalOptions);
|
|
321
1001
|
}
|
|
322
1002
|
};
|
|
323
1003
|
};
|
|
324
1004
|
export const createSqliteAgentMemoryStore = (options) => {
|
|
325
1005
|
const tableName = validateIdentifier(options.tableName ?? "zhivex_agent_memory", "tableName");
|
|
326
|
-
const keyFor = options.key ?? defaultMemoryKey;
|
|
1006
|
+
const keyFor = (context) => (options.key ?? defaultMemoryKey)({ ...context, scope: resolveScope(options.scope, context.scope) });
|
|
327
1007
|
const selectMessages = options.selectMessages ?? defaultMemoryMessages;
|
|
328
1008
|
initializeSqliteTable(options.db, `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
329
1009
|
memory_key TEXT PRIMARY KEY,
|
|
@@ -354,6 +1034,9 @@ export const createPostgresAgentRunStore = (options) => {
|
|
|
354
1034
|
const tableName = validateIdentifier(options.tableName ?? "zhivex_agent_runs", "tableName");
|
|
355
1035
|
const idempotencyTableName = `${tableName}_idempotency`;
|
|
356
1036
|
const parentTableName = `${tableName}_parents`;
|
|
1037
|
+
const leaseTableName = `${tableName}_leases`;
|
|
1038
|
+
const journalTableName = `${tableName}_tool_journal`;
|
|
1039
|
+
const dbKey = (value, scope) => scopedKey(resolveScope(options.scope, scope), value);
|
|
357
1040
|
const createSql = `
|
|
358
1041
|
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
359
1042
|
run_id TEXT PRIMARY KEY,
|
|
@@ -375,76 +1058,275 @@ export const createPostgresAgentRunStore = (options) => {
|
|
|
375
1058
|
updated_at_ms BIGINT NOT NULL
|
|
376
1059
|
)
|
|
377
1060
|
`;
|
|
1061
|
+
const createLeaseSql = `CREATE TABLE IF NOT EXISTS ${leaseTableName} (
|
|
1062
|
+
run_key TEXT PRIMARY KEY,
|
|
1063
|
+
run_id TEXT NOT NULL,
|
|
1064
|
+
owner_id TEXT NOT NULL,
|
|
1065
|
+
expires_at_ms BIGINT NOT NULL
|
|
1066
|
+
)`;
|
|
1067
|
+
const createJournalSql = `CREATE TABLE IF NOT EXISTS ${journalTableName} (
|
|
1068
|
+
run_key TEXT NOT NULL,
|
|
1069
|
+
tool_call_id TEXT NOT NULL,
|
|
1070
|
+
entry_json JSONB NOT NULL,
|
|
1071
|
+
revision BIGINT NOT NULL,
|
|
1072
|
+
updated_at_ms BIGINT NOT NULL,
|
|
1073
|
+
PRIMARY KEY (run_key, tool_call_id)
|
|
1074
|
+
)`;
|
|
1075
|
+
const createIndexesSql = `
|
|
1076
|
+
CREATE INDEX IF NOT EXISTS ${tableName}_updated_idx ON ${tableName} (updated_at_ms DESC, run_id);
|
|
1077
|
+
CREATE INDEX IF NOT EXISTS ${parentTableName}_parent_idx ON ${parentTableName} (parent_run_id, updated_at_ms DESC);
|
|
1078
|
+
CREATE INDEX IF NOT EXISTS ${leaseTableName}_expiry_idx ON ${leaseTableName} (expires_at_ms);
|
|
1079
|
+
CREATE INDEX IF NOT EXISTS ${journalTableName}_run_idx ON ${journalTableName} (run_key, updated_at_ms, tool_call_id)
|
|
1080
|
+
`;
|
|
1081
|
+
const ensureAllTables = async () => {
|
|
1082
|
+
await ensurePostgresTable(options.client, tableName, createSql);
|
|
1083
|
+
await ensurePostgresTable(options.client, idempotencyTableName, createIdempotencySql);
|
|
1084
|
+
await ensurePostgresTable(options.client, parentTableName, createParentSql);
|
|
1085
|
+
await ensurePostgresTable(options.client, leaseTableName, createLeaseSql);
|
|
1086
|
+
await ensurePostgresTable(options.client, journalTableName, createJournalSql);
|
|
1087
|
+
await ensurePostgresTable(options.client, `${tableName}:indexes`, createIndexesSql);
|
|
1088
|
+
};
|
|
378
1089
|
return {
|
|
379
|
-
async load(runId) {
|
|
380
|
-
await
|
|
381
|
-
const result = await options.client.query(`SELECT state_json FROM ${tableName} WHERE run_id = $1`, [runId]);
|
|
1090
|
+
async load(runId, scope) {
|
|
1091
|
+
await ensureAllTables();
|
|
1092
|
+
const result = await options.client.query(`SELECT state_json FROM ${tableName} WHERE run_id = $1`, [dbKey(runId, scope)]);
|
|
382
1093
|
const state = result.rows[0] ? (getRecordField(result.rows[0], ["state_json", "stateJson"]) ?? undefined) : undefined;
|
|
383
|
-
return state ?
|
|
1094
|
+
return state ? normalizeAgentRunState(state) : undefined;
|
|
384
1095
|
},
|
|
385
|
-
async findByIdempotencyKey(idempotencyKey) {
|
|
386
|
-
await
|
|
387
|
-
await ensurePostgresTable(options.client, idempotencyTableName, createIdempotencySql);
|
|
1096
|
+
async findByIdempotencyKey(idempotencyKey, scope) {
|
|
1097
|
+
await ensureAllTables();
|
|
388
1098
|
const result = await options.client.query(`SELECT runs.state_json
|
|
389
1099
|
FROM ${tableName} runs
|
|
390
1100
|
INNER JOIN ${idempotencyTableName} keys ON keys.run_id = runs.run_id
|
|
391
|
-
WHERE keys.idempotency_key = $1`, [idempotencyKey]);
|
|
1101
|
+
WHERE keys.idempotency_key = $1`, [dbKey(idempotencyKey, scope)]);
|
|
392
1102
|
const state = result.rows[0] ? (getRecordField(result.rows[0], ["state_json", "stateJson"]) ?? undefined) : undefined;
|
|
393
|
-
return state ?
|
|
1103
|
+
return state ? normalizeAgentRunState(state) : undefined;
|
|
394
1104
|
},
|
|
395
|
-
async findByParentRunId(parentRunId) {
|
|
396
|
-
await
|
|
397
|
-
await ensurePostgresTable(options.client, parentTableName, createParentSql);
|
|
1105
|
+
async findByParentRunId(parentRunId, scope) {
|
|
1106
|
+
await ensureAllTables();
|
|
398
1107
|
const result = await options.client.query(`SELECT runs.state_json
|
|
399
1108
|
FROM ${tableName} runs
|
|
400
1109
|
INNER JOIN ${parentTableName} parents ON parents.run_id = runs.run_id
|
|
401
|
-
WHERE parents.parent_run_id = $1`, [parentRunId]);
|
|
1110
|
+
WHERE parents.parent_run_id = $1`, [dbKey(parentRunId, scope)]);
|
|
402
1111
|
return result.rows.flatMap((row) => {
|
|
403
1112
|
const state = getRecordField(row, ["state_json", "stateJson"]) ?? undefined;
|
|
404
|
-
return state ? [
|
|
1113
|
+
return state ? [normalizeAgentRunState(state)] : [];
|
|
405
1114
|
});
|
|
406
1115
|
},
|
|
407
|
-
async
|
|
408
|
-
await
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const normalized = normalizeRunState(state);
|
|
1116
|
+
async claimIdempotencyKey(state) {
|
|
1117
|
+
await ensureAllTables();
|
|
1118
|
+
const scope = resolveScope(options.scope, state.scope);
|
|
1119
|
+
const normalized = normalizeAgentRunState({ ...state, ...(scope ? { scope } : {}) });
|
|
412
1120
|
const updatedAt = Date.now();
|
|
413
1121
|
await options.client.query(`INSERT INTO ${tableName} (run_id, state_json, updated_at_ms)
|
|
414
1122
|
VALUES ($1, $2::jsonb, $3)
|
|
415
|
-
ON CONFLICT(run_id) DO
|
|
416
|
-
|
|
417
|
-
|
|
1123
|
+
ON CONFLICT(run_id) DO NOTHING`, [dbKey(normalized.runId, scope), JSON.stringify(normalized), updatedAt]);
|
|
1124
|
+
const claim = await options.client.query(`INSERT INTO ${idempotencyTableName} (idempotency_key, run_id, updated_at_ms)
|
|
1125
|
+
VALUES ($1, $2, $3)
|
|
1126
|
+
ON CONFLICT(idempotency_key) DO NOTHING
|
|
1127
|
+
RETURNING run_id`, [dbKey(state.idempotencyKey, scope), dbKey(normalized.runId, scope), updatedAt]);
|
|
1128
|
+
const claimedRunId = getRecordField(claim.rows[0], ["run_id", "runId"]);
|
|
1129
|
+
if (claimedRunId === dbKey(normalized.runId, scope)) {
|
|
1130
|
+
if (normalized.parentRunId) {
|
|
1131
|
+
await options.client.query(`INSERT INTO ${parentTableName} (run_id, parent_run_id, updated_at_ms)
|
|
1132
|
+
VALUES ($1, $2, $3)
|
|
1133
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
1134
|
+
parent_run_id = EXCLUDED.parent_run_id,
|
|
1135
|
+
updated_at_ms = EXCLUDED.updated_at_ms`, [dbKey(normalized.runId, scope), dbKey(normalized.parentRunId, scope), updatedAt]);
|
|
1136
|
+
}
|
|
1137
|
+
return { claimed: true, state: normalized };
|
|
1138
|
+
}
|
|
1139
|
+
const existing = await options.client.query(`SELECT runs.state_json
|
|
1140
|
+
FROM ${tableName} runs
|
|
1141
|
+
INNER JOIN ${idempotencyTableName} keys ON keys.run_id = runs.run_id
|
|
1142
|
+
WHERE keys.idempotency_key = $1`, [dbKey(state.idempotencyKey, scope)]);
|
|
1143
|
+
const existingState = existing.rows[0]
|
|
1144
|
+
? getRecordField(existing.rows[0], ["state_json", "stateJson"])
|
|
1145
|
+
: undefined;
|
|
1146
|
+
if (!existingState) {
|
|
1147
|
+
throw new ConflictError("AgentRunState idempotency claim could not be loaded.");
|
|
1148
|
+
}
|
|
1149
|
+
if (existingState.runId !== normalized.runId) {
|
|
1150
|
+
await options.client.query(`DELETE FROM ${tableName} WHERE run_id = $1`, [dbKey(normalized.runId, scope)]);
|
|
1151
|
+
}
|
|
1152
|
+
return { claimed: false, state: normalizeAgentRunState(existingState) };
|
|
1153
|
+
},
|
|
1154
|
+
async save(state, saveOptions) {
|
|
1155
|
+
await ensureAllTables();
|
|
1156
|
+
const scope = resolveScope(options.scope, state.scope);
|
|
1157
|
+
const normalized = nextStoredState(state, saveOptions);
|
|
1158
|
+
const stored = { ...normalized, ...(scope ? { scope } : {}) };
|
|
1159
|
+
const updatedAt = Date.now();
|
|
1160
|
+
if (normalized.idempotencyKey) {
|
|
1161
|
+
const owner = await options.client.query(`SELECT run_id FROM ${idempotencyTableName} WHERE idempotency_key = $1`, [dbKey(normalized.idempotencyKey, scope)]);
|
|
1162
|
+
const ownerRunId = getRecordField(owner.rows[0], ["run_id", "runId"]);
|
|
1163
|
+
if (typeof ownerRunId === "string" && ownerRunId !== dbKey(normalized.runId, scope)) {
|
|
1164
|
+
throw new ConflictError("AgentRunState idempotency key conflict.");
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (saveOptions?.expectedRevision === undefined) {
|
|
1168
|
+
await options.client.query(`INSERT INTO ${tableName} (run_id, state_json, updated_at_ms)
|
|
1169
|
+
VALUES ($1, $2::jsonb, $3)
|
|
1170
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
1171
|
+
state_json = EXCLUDED.state_json,
|
|
1172
|
+
updated_at_ms = EXCLUDED.updated_at_ms`, [dbKey(normalized.runId, scope), JSON.stringify(stored), updatedAt]);
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
const saved = await options.client.query(`INSERT INTO ${tableName} (run_id, state_json, updated_at_ms)
|
|
1176
|
+
VALUES ($1, $2::jsonb, $3)
|
|
1177
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
1178
|
+
state_json = EXCLUDED.state_json,
|
|
1179
|
+
updated_at_ms = EXCLUDED.updated_at_ms
|
|
1180
|
+
WHERE COALESCE((${tableName}.state_json->>'revision')::bigint, 0) = $4
|
|
1181
|
+
RETURNING run_id`, [dbKey(normalized.runId, scope), JSON.stringify(stored), updatedAt, saveOptions.expectedRevision]);
|
|
1182
|
+
if (getRecordField(saved.rows[0], ["run_id", "runId"]) !== dbKey(normalized.runId, scope)) {
|
|
1183
|
+
throw new ConflictError("AgentRunState revision conflict.");
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
418
1186
|
if (normalized.idempotencyKey) {
|
|
419
1187
|
await options.client.query(`INSERT INTO ${idempotencyTableName} (idempotency_key, run_id, updated_at_ms)
|
|
420
1188
|
VALUES ($1, $2, $3)
|
|
421
|
-
ON CONFLICT(idempotency_key) DO
|
|
422
|
-
run_id = EXCLUDED.run_id,
|
|
423
|
-
updated_at_ms = EXCLUDED.updated_at_ms`, [normalized.idempotencyKey, normalized.runId, updatedAt]);
|
|
1189
|
+
ON CONFLICT(idempotency_key) DO NOTHING`, [dbKey(normalized.idempotencyKey, scope), dbKey(normalized.runId, scope), updatedAt]);
|
|
424
1190
|
}
|
|
425
|
-
await options.client.query(`DELETE FROM ${parentTableName} WHERE run_id = $1`, [normalized.runId]);
|
|
1191
|
+
await options.client.query(`DELETE FROM ${parentTableName} WHERE run_id = $1`, [dbKey(normalized.runId, scope)]);
|
|
426
1192
|
if (normalized.parentRunId) {
|
|
427
1193
|
await options.client.query(`INSERT INTO ${parentTableName} (run_id, parent_run_id, updated_at_ms)
|
|
428
1194
|
VALUES ($1, $2, $3)
|
|
429
1195
|
ON CONFLICT(run_id) DO UPDATE SET
|
|
430
1196
|
parent_run_id = EXCLUDED.parent_run_id,
|
|
431
|
-
updated_at_ms = EXCLUDED.updated_at_ms`, [normalized.runId, normalized.parentRunId, updatedAt]);
|
|
1197
|
+
updated_at_ms = EXCLUDED.updated_at_ms`, [dbKey(normalized.runId, scope), dbKey(normalized.parentRunId, scope), updatedAt]);
|
|
432
1198
|
}
|
|
433
1199
|
},
|
|
434
|
-
async delete(runId) {
|
|
435
|
-
await
|
|
436
|
-
|
|
437
|
-
await
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1200
|
+
async delete(runId, scope) {
|
|
1201
|
+
await ensureAllTables();
|
|
1202
|
+
const key = dbKey(runId, scope);
|
|
1203
|
+
await options.client.query(`WITH deleted_run AS (
|
|
1204
|
+
DELETE FROM ${tableName} WHERE run_id = $1 RETURNING run_id
|
|
1205
|
+
), deleted_idempotency AS (
|
|
1206
|
+
DELETE FROM ${idempotencyTableName} WHERE run_id = $1 RETURNING run_id
|
|
1207
|
+
), deleted_parent AS (
|
|
1208
|
+
DELETE FROM ${parentTableName} WHERE run_id = $1 RETURNING run_id
|
|
1209
|
+
), deleted_lease AS (
|
|
1210
|
+
DELETE FROM ${leaseTableName} WHERE run_key = $1 RETURNING run_key
|
|
1211
|
+
)
|
|
1212
|
+
DELETE FROM ${journalTableName} WHERE run_key = $1`, [key]);
|
|
1213
|
+
},
|
|
1214
|
+
async list(listOptions, scope) {
|
|
1215
|
+
await ensureAllTables();
|
|
1216
|
+
const prefix = scopePrefix(resolveScope(options.scope, scope));
|
|
1217
|
+
const result = await options.client.query(`SELECT state_json FROM ${tableName} WHERE run_id >= $1 AND run_id < $2`, [prefix, `${prefix}\uffff`]);
|
|
1218
|
+
const states = result.rows.flatMap((row) => {
|
|
1219
|
+
const state = getRecordField(row, ["state_json", "stateJson"]);
|
|
1220
|
+
return state ? [normalizeAgentRunState(state)] : [];
|
|
1221
|
+
});
|
|
1222
|
+
return listStates(states, listOptions);
|
|
1223
|
+
},
|
|
1224
|
+
async deleteExpired(retention, scope) {
|
|
1225
|
+
const page = await this.list?.({ statuses: retention.statuses, updatedBefore: retention.before, limit: retention.limit ?? 1_000 }, scope);
|
|
1226
|
+
for (const state of page.items)
|
|
1227
|
+
await this.delete?.(state.runId, state.scope);
|
|
1228
|
+
return page.items.length;
|
|
1229
|
+
},
|
|
1230
|
+
async acquireLease(runId, leaseOptions, scope) {
|
|
1231
|
+
validateLeaseOptions(leaseOptions);
|
|
1232
|
+
await ensureAllTables();
|
|
1233
|
+
const now = leaseOptions.now ?? Date.now();
|
|
1234
|
+
const key = dbKey(runId, scope);
|
|
1235
|
+
const expiresAt = now + leaseOptions.ttlMs;
|
|
1236
|
+
const result = await options.client.query(`INSERT INTO ${leaseTableName} (run_key, run_id, owner_id, expires_at_ms)
|
|
1237
|
+
SELECT $1, $2, $3, $4
|
|
1238
|
+
WHERE EXISTS (SELECT 1 FROM ${tableName} WHERE run_id = $1)
|
|
1239
|
+
ON CONFLICT(run_key) DO UPDATE SET
|
|
1240
|
+
owner_id = EXCLUDED.owner_id,
|
|
1241
|
+
expires_at_ms = EXCLUDED.expires_at_ms
|
|
1242
|
+
WHERE ${leaseTableName}.owner_id = EXCLUDED.owner_id OR ${leaseTableName}.expires_at_ms <= $5
|
|
1243
|
+
RETURNING owner_id, expires_at_ms`, [key, runId, leaseOptions.ownerId, expiresAt, now]);
|
|
1244
|
+
const owner = getRecordField(result.rows[0], ["owner_id", "ownerId"]);
|
|
1245
|
+
return owner === leaseOptions.ownerId ? { runId, ownerId: leaseOptions.ownerId, expiresAt } : undefined;
|
|
1246
|
+
},
|
|
1247
|
+
async renewLease(runId, leaseOptions, scope) {
|
|
1248
|
+
validateLeaseOptions(leaseOptions);
|
|
1249
|
+
await ensureAllTables();
|
|
1250
|
+
const now = leaseOptions.now ?? Date.now();
|
|
1251
|
+
const expiresAt = now + leaseOptions.ttlMs;
|
|
1252
|
+
const result = await options.client.query(`UPDATE ${leaseTableName}
|
|
1253
|
+
SET expires_at_ms = $3
|
|
1254
|
+
WHERE run_key = $1 AND owner_id = $2 AND expires_at_ms > $4
|
|
1255
|
+
RETURNING owner_id`, [dbKey(runId, scope), leaseOptions.ownerId, expiresAt, now]);
|
|
1256
|
+
return getRecordField(result.rows[0], ["owner_id", "ownerId"]) === leaseOptions.ownerId
|
|
1257
|
+
? { runId, ownerId: leaseOptions.ownerId, expiresAt }
|
|
1258
|
+
: undefined;
|
|
1259
|
+
},
|
|
1260
|
+
async releaseLease(runId, ownerId, scope) {
|
|
1261
|
+
await ensureAllTables();
|
|
1262
|
+
const result = await options.client.query(`DELETE FROM ${leaseTableName} WHERE run_key = $1 AND owner_id = $2 RETURNING owner_id`, [dbKey(runId, scope), ownerId]);
|
|
1263
|
+
return getRecordField(result.rows[0], ["owner_id", "ownerId"]) === ownerId;
|
|
1264
|
+
},
|
|
1265
|
+
async loadToolCall(runId, toolCallId, scope) {
|
|
1266
|
+
await ensureAllTables();
|
|
1267
|
+
const result = await options.client.query(`SELECT entry_json FROM ${journalTableName} WHERE run_key = $1 AND tool_call_id = $2`, [dbKey(runId, scope), toolCallId]);
|
|
1268
|
+
const entry = getRecordField(result.rows[0], ["entry_json", "entryJson"]);
|
|
1269
|
+
return entry && typeof entry === "object" ? cloneJournalEntry(entry) : undefined;
|
|
1270
|
+
},
|
|
1271
|
+
async loadToolExecution(runId, toolCallId, scope) {
|
|
1272
|
+
return this.loadToolCall?.(runId, toolCallId, scope);
|
|
1273
|
+
},
|
|
1274
|
+
async listToolCalls(runId, scope) {
|
|
1275
|
+
await ensureAllTables();
|
|
1276
|
+
const result = await options.client.query(`SELECT entry_json FROM ${journalTableName} WHERE run_key = $1 ORDER BY updated_at_ms, tool_call_id`, [dbKey(runId, scope)]);
|
|
1277
|
+
return result.rows.flatMap((row) => {
|
|
1278
|
+
const entry = getRecordField(row, ["entry_json", "entryJson"]);
|
|
1279
|
+
return entry && typeof entry === "object" ? [cloneJournalEntry(entry)] : [];
|
|
1280
|
+
});
|
|
1281
|
+
},
|
|
1282
|
+
async saveToolCall(entry, journalOptions) {
|
|
1283
|
+
await ensureAllTables();
|
|
1284
|
+
const next = nextJournalEntry(entry, journalOptions);
|
|
1285
|
+
const key = dbKey(entry.runId, entry.scope);
|
|
1286
|
+
const result = journalOptions?.expectedRevision === undefined
|
|
1287
|
+
? await options.client.query(`INSERT INTO ${journalTableName} (run_key, tool_call_id, entry_json, revision, updated_at_ms)
|
|
1288
|
+
VALUES ($1, $2, $3::jsonb, $4, $5)
|
|
1289
|
+
ON CONFLICT(run_key, tool_call_id) DO UPDATE SET
|
|
1290
|
+
entry_json = EXCLUDED.entry_json,
|
|
1291
|
+
revision = EXCLUDED.revision,
|
|
1292
|
+
updated_at_ms = EXCLUDED.updated_at_ms
|
|
1293
|
+
RETURNING revision`, [key, entry.toolCallId, JSON.stringify(next), next.revision, next.updatedAt])
|
|
1294
|
+
: await options.client.query(`UPDATE ${journalTableName}
|
|
1295
|
+
SET entry_json = $3::jsonb,
|
|
1296
|
+
revision = $4,
|
|
1297
|
+
updated_at_ms = $5
|
|
1298
|
+
WHERE run_key = $1 AND tool_call_id = $2 AND revision = $6
|
|
1299
|
+
RETURNING revision`, [key, entry.toolCallId, JSON.stringify(next), next.revision, next.updatedAt, journalOptions.expectedRevision]);
|
|
1300
|
+
if (getRecordField(result.rows[0], ["revision"]) === undefined) {
|
|
1301
|
+
throw new ConflictError("Agent tool-call journal revision conflict.");
|
|
1302
|
+
}
|
|
1303
|
+
return next;
|
|
1304
|
+
},
|
|
1305
|
+
async claimToolExecution(entry) {
|
|
1306
|
+
await ensureAllTables();
|
|
1307
|
+
const next = nextJournalEntry({ ...entry, status: "running", revision: 0 });
|
|
1308
|
+
const result = await options.client.query(`INSERT INTO ${journalTableName} (run_key, tool_call_id, entry_json, revision, updated_at_ms)
|
|
1309
|
+
SELECT $1, $2, $3::jsonb, 0, $4
|
|
1310
|
+
WHERE EXISTS (SELECT 1 FROM ${tableName} WHERE run_id = $1)
|
|
1311
|
+
ON CONFLICT(run_key, tool_call_id) DO NOTHING
|
|
1312
|
+
RETURNING entry_json`, [dbKey(entry.runId, entry.scope), entry.toolCallId, JSON.stringify(next), next.updatedAt]);
|
|
1313
|
+
const claimed = getRecordField(result.rows[0], ["entry_json", "entryJson"]);
|
|
1314
|
+
if (claimed && typeof claimed === "object")
|
|
1315
|
+
return { claimed: true, entry: next };
|
|
1316
|
+
const existing = await this.loadToolCall?.(entry.runId, entry.toolCallId, entry.scope);
|
|
1317
|
+
if (!existing)
|
|
1318
|
+
throw new ValidationError("Cannot journal a tool call for an unknown run.");
|
|
1319
|
+
return { claimed: false, entry: existing };
|
|
1320
|
+
},
|
|
1321
|
+
async completeToolExecution(entry, journalOptions) {
|
|
1322
|
+
return this.saveToolCall?.({ ...entry, status: entry.status === "failed" ? "failed" : "completed" }, journalOptions);
|
|
441
1323
|
}
|
|
442
1324
|
};
|
|
443
1325
|
};
|
|
444
1326
|
export const createPostgresAgentMemoryStore = (options) => {
|
|
445
1327
|
assertPostgresClient(options.client);
|
|
446
1328
|
const tableName = validateIdentifier(options.tableName ?? "zhivex_agent_memory", "tableName");
|
|
447
|
-
const keyFor = options.key ?? defaultMemoryKey;
|
|
1329
|
+
const keyFor = (context) => (options.key ?? defaultMemoryKey)({ ...context, scope: resolveScope(options.scope, context.scope) });
|
|
448
1330
|
const selectMessages = options.selectMessages ?? defaultMemoryMessages;
|
|
449
1331
|
const createSql = `
|
|
450
1332
|
CREATE TABLE IF NOT EXISTS ${tableName} (
|