@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/runner.js
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resumeAgent, runAgent, streamAgent } from "./agent.js";
|
|
4
|
+
import { ConflictError, ValidationError } from "./errors.js";
|
|
5
|
+
import { normalizeMessages } from "./generate-text.js";
|
|
6
|
+
import { createTextMessage, serializeJsonValue } from "./messages.js";
|
|
7
|
+
const randomId = (prefix) => `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
8
|
+
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
|
|
9
|
+
export const SESSION_SCHEMA_VERSION = 1;
|
|
10
|
+
const sessionKey = (input) => `${input.appName}:${input.userId}:${input.sessionId}`;
|
|
11
|
+
const identifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
12
|
+
const validateIdentifier = (value, fieldName) => {
|
|
13
|
+
if (!identifierPattern.test(value)) {
|
|
14
|
+
throw new ValidationError(`The "${fieldName}" option must match the SQL identifier pattern [A-Za-z_][A-Za-z0-9_]*.`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
const getRecordField = (value, candidates) => {
|
|
19
|
+
if (!value || typeof value !== "object") {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const record = value;
|
|
23
|
+
for (const candidate of candidates) {
|
|
24
|
+
if (candidate in record) {
|
|
25
|
+
return record[candidate];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
const parseSessionJson = (value) => {
|
|
31
|
+
if (!value) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
return normalizeAgentSession(JSON.parse(value));
|
|
36
|
+
}
|
|
37
|
+
return normalizeAgentSession(value);
|
|
38
|
+
};
|
|
39
|
+
export const normalizeAgentSession = (value) => {
|
|
40
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
41
|
+
throw new ValidationError("AgentSession must be an object.");
|
|
42
|
+
}
|
|
43
|
+
const session = value;
|
|
44
|
+
if (session.schemaVersion !== undefined && session.schemaVersion > SESSION_SCHEMA_VERSION) {
|
|
45
|
+
throw new ValidationError(`Unsupported AgentSession schemaVersion ${session.schemaVersion}.`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof session.appName !== "string" ||
|
|
48
|
+
typeof session.userId !== "string" ||
|
|
49
|
+
typeof session.sessionId !== "string" ||
|
|
50
|
+
typeof session.createdAt !== "number" ||
|
|
51
|
+
typeof session.updatedAt !== "number" ||
|
|
52
|
+
!Array.isArray(session.events)) {
|
|
53
|
+
throw new ValidationError("AgentSession is missing required fields.");
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
57
|
+
revision: typeof session.revision === "number" ? session.revision : 1,
|
|
58
|
+
appName: session.appName,
|
|
59
|
+
userId: session.userId,
|
|
60
|
+
sessionId: session.sessionId,
|
|
61
|
+
createdAt: session.createdAt,
|
|
62
|
+
updatedAt: session.updatedAt,
|
|
63
|
+
metadata: session.metadata ? cloneJson(session.metadata) : undefined,
|
|
64
|
+
events: cloneJson(session.events),
|
|
65
|
+
lastRunState: session.lastRunState ? cloneJson(session.lastRunState) : undefined
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
export const migrateAgentSessionRecord = (value, targetVersion = SESSION_SCHEMA_VERSION) => {
|
|
69
|
+
if (targetVersion !== SESSION_SCHEMA_VERSION) {
|
|
70
|
+
throw new ValidationError(`Unsupported AgentSession migration target ${targetVersion}.`);
|
|
71
|
+
}
|
|
72
|
+
return normalizeAgentSession(value);
|
|
73
|
+
};
|
|
74
|
+
const assertExpectedRevision = (current, expectedRevision, resource) => {
|
|
75
|
+
if (expectedRevision !== undefined && (current?.revision ?? 0) !== expectedRevision) {
|
|
76
|
+
throw new ConflictError(`${resource} revision conflict.`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const sqliteMutationCount = (result) => {
|
|
80
|
+
if (!result || typeof result !== "object") {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const record = result;
|
|
84
|
+
const value = record.changes ?? record.changeset ?? record.rowCount;
|
|
85
|
+
return typeof value === "number" ? value : undefined;
|
|
86
|
+
};
|
|
87
|
+
const prepareSqliteStatement = (db, sql) => {
|
|
88
|
+
if (typeof db.prepare === "function") {
|
|
89
|
+
return db.prepare(sql);
|
|
90
|
+
}
|
|
91
|
+
if (typeof db.query === "function") {
|
|
92
|
+
return db.query(sql);
|
|
93
|
+
}
|
|
94
|
+
throw new ValidationError('The "db" option must expose either a "prepare()" or "query()" method.');
|
|
95
|
+
};
|
|
96
|
+
const ensurePostgresTable = (() => {
|
|
97
|
+
const initializedTables = new WeakMap();
|
|
98
|
+
return async (client, tableName, createSql) => {
|
|
99
|
+
let tables = initializedTables.get(client);
|
|
100
|
+
if (!tables) {
|
|
101
|
+
tables = new Map();
|
|
102
|
+
initializedTables.set(client, tables);
|
|
103
|
+
}
|
|
104
|
+
let initialization = tables.get(tableName);
|
|
105
|
+
if (!initialization) {
|
|
106
|
+
initialization = Promise.resolve(client.query(createSql, [])).then(() => undefined);
|
|
107
|
+
tables.set(tableName, initialization);
|
|
108
|
+
}
|
|
109
|
+
await initialization;
|
|
110
|
+
};
|
|
111
|
+
})();
|
|
112
|
+
const toSessionEvent = (type, input) => ({
|
|
113
|
+
...input,
|
|
114
|
+
id: randomId("evt"),
|
|
115
|
+
type,
|
|
116
|
+
createdAt: Date.now()
|
|
117
|
+
});
|
|
118
|
+
const cloneSession = (session) => cloneJson(normalizeAgentSession(session));
|
|
119
|
+
export const createInMemorySessionService = () => {
|
|
120
|
+
const sessions = new Map();
|
|
121
|
+
return {
|
|
122
|
+
loadSession(input) {
|
|
123
|
+
const session = sessions.get(sessionKey(input));
|
|
124
|
+
return session ? cloneSession(session) : undefined;
|
|
125
|
+
},
|
|
126
|
+
createSession(input) {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
const session = {
|
|
129
|
+
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
130
|
+
revision: 1,
|
|
131
|
+
appName: input.appName,
|
|
132
|
+
userId: input.userId,
|
|
133
|
+
sessionId: input.sessionId,
|
|
134
|
+
createdAt: now,
|
|
135
|
+
updatedAt: now,
|
|
136
|
+
metadata: input.metadata ? cloneJson(input.metadata) : undefined,
|
|
137
|
+
events: []
|
|
138
|
+
};
|
|
139
|
+
sessions.set(sessionKey(input), cloneSession(session));
|
|
140
|
+
return cloneSession(session);
|
|
141
|
+
},
|
|
142
|
+
saveSession(session, options) {
|
|
143
|
+
assertExpectedRevision(sessions.get(sessionKey(session)), options?.expectedRevision, "AgentSession");
|
|
144
|
+
const normalized = normalizeAgentSession({ ...session, updatedAt: Date.now() });
|
|
145
|
+
const current = sessions.get(sessionKey(normalized));
|
|
146
|
+
sessions.set(sessionKey(normalized), cloneSession({
|
|
147
|
+
...normalized,
|
|
148
|
+
revision: (current?.revision ?? normalized.revision) + 1
|
|
149
|
+
}));
|
|
150
|
+
},
|
|
151
|
+
appendEvent(input) {
|
|
152
|
+
const existing = sessions.get(sessionKey(input));
|
|
153
|
+
const base = existing ??
|
|
154
|
+
{
|
|
155
|
+
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
156
|
+
revision: 1,
|
|
157
|
+
appName: input.appName,
|
|
158
|
+
userId: input.userId,
|
|
159
|
+
sessionId: input.sessionId,
|
|
160
|
+
createdAt: Date.now(),
|
|
161
|
+
updatedAt: Date.now(),
|
|
162
|
+
events: []
|
|
163
|
+
};
|
|
164
|
+
const next = {
|
|
165
|
+
...base,
|
|
166
|
+
revision: existing ? base.revision + 1 : 1,
|
|
167
|
+
updatedAt: Date.now(),
|
|
168
|
+
events: [...base.events, cloneJson(input.event)]
|
|
169
|
+
};
|
|
170
|
+
assertExpectedRevision(existing, input.expectedRevision, "AgentSession");
|
|
171
|
+
sessions.set(sessionKey(input), cloneSession(next));
|
|
172
|
+
return cloneSession(next);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const createEmptySession = (input) => {
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
return {
|
|
179
|
+
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
180
|
+
revision: 1,
|
|
181
|
+
appName: input.appName,
|
|
182
|
+
userId: input.userId,
|
|
183
|
+
sessionId: input.sessionId,
|
|
184
|
+
createdAt: now,
|
|
185
|
+
updatedAt: now,
|
|
186
|
+
metadata: input.metadata ? cloneJson(input.metadata) : undefined,
|
|
187
|
+
events: []
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
const appendEventToSession = (session, event) => ({
|
|
191
|
+
...normalizeAgentSession(session),
|
|
192
|
+
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
193
|
+
updatedAt: Date.now(),
|
|
194
|
+
events: [...session.events, cloneJson(event)]
|
|
195
|
+
});
|
|
196
|
+
const fileNameForSession = (input) => [input.appName, input.userId, input.sessionId].map((part) => encodeURIComponent(part)).join("__") + ".json";
|
|
197
|
+
export const createFileSessionService = (options) => {
|
|
198
|
+
const filePath = (input) => path.join(options.directory, fileNameForSession(input));
|
|
199
|
+
const load = async (input) => {
|
|
200
|
+
try {
|
|
201
|
+
const content = await fs.readFile(filePath(input), "utf8");
|
|
202
|
+
return normalizeAgentSession(JSON.parse(content));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error.code === "ENOENT") {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const save = async (session, saveOptions) => {
|
|
212
|
+
const existing = await load(session);
|
|
213
|
+
assertExpectedRevision(existing, saveOptions?.expectedRevision, "AgentSession");
|
|
214
|
+
const next = normalizeAgentSession({ ...session, schemaVersion: SESSION_SCHEMA_VERSION, updatedAt: Date.now() });
|
|
215
|
+
next.revision = existing ? existing.revision + 1 : next.revision;
|
|
216
|
+
await fs.mkdir(options.directory, { recursive: true });
|
|
217
|
+
await fs.writeFile(filePath(next), JSON.stringify(next, null, 2), "utf8");
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
async loadSession(input) {
|
|
221
|
+
return load(input);
|
|
222
|
+
},
|
|
223
|
+
async createSession(input) {
|
|
224
|
+
const session = createEmptySession(input);
|
|
225
|
+
await save(session);
|
|
226
|
+
return cloneSession(session);
|
|
227
|
+
},
|
|
228
|
+
async saveSession(session, options) {
|
|
229
|
+
await save(session, options);
|
|
230
|
+
},
|
|
231
|
+
async appendEvent(input) {
|
|
232
|
+
const existing = await load(input);
|
|
233
|
+
assertExpectedRevision(existing, input.expectedRevision, "AgentSession");
|
|
234
|
+
const base = existing ?? createEmptySession(input);
|
|
235
|
+
const next = appendEventToSession(base, input.event);
|
|
236
|
+
await save(next);
|
|
237
|
+
return cloneSession(next);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
export const pruneFileSessionStore = async (options) => {
|
|
242
|
+
const now = options.now ?? Date.now();
|
|
243
|
+
const dryRun = options.dryRun ?? true;
|
|
244
|
+
let entries;
|
|
245
|
+
try {
|
|
246
|
+
entries = await fs.readdir(options.directory);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
if (error.code === "ENOENT") {
|
|
250
|
+
return { directory: options.directory, dryRun, deletedSessionKeys: [], keptSessionKeys: [] };
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
const sessions = [];
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
if (!entry.endsWith(".json")) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const filePath = path.join(options.directory, entry);
|
|
260
|
+
const session = normalizeAgentSession(JSON.parse(await fs.readFile(filePath, "utf8")));
|
|
261
|
+
sessions.push({
|
|
262
|
+
filePath,
|
|
263
|
+
key: sessionKey(session),
|
|
264
|
+
updatedAt: session.updatedAt
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const sorted = sessions.sort((left, right) => right.updatedAt - left.updatedAt || left.key.localeCompare(right.key));
|
|
268
|
+
const keepByCount = new Set(options.keepLast === undefined ? [] : sorted.slice(0, Math.max(0, options.keepLast)).map((session) => session.key));
|
|
269
|
+
const shouldDelete = (session) => !keepByCount.has(session.key) &&
|
|
270
|
+
(options.olderThanMs !== undefined ? now - session.updatedAt > options.olderThanMs : options.keepLast !== undefined);
|
|
271
|
+
const deleted = sorted.filter(shouldDelete);
|
|
272
|
+
if (!dryRun) {
|
|
273
|
+
for (const session of deleted) {
|
|
274
|
+
await fs.unlink(session.filePath);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
directory: options.directory,
|
|
279
|
+
dryRun,
|
|
280
|
+
deletedSessionKeys: deleted.map((session) => session.key),
|
|
281
|
+
keptSessionKeys: sorted.filter((session) => !shouldDelete(session)).map((session) => session.key)
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
export const createSqliteSessionService = (options) => {
|
|
285
|
+
const tableName = validateIdentifier(options.tableName ?? "zhivex_agent_sessions", "tableName");
|
|
286
|
+
options.db.exec(`
|
|
287
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
288
|
+
session_key TEXT PRIMARY KEY,
|
|
289
|
+
app_name TEXT NOT NULL,
|
|
290
|
+
user_id TEXT NOT NULL,
|
|
291
|
+
session_id TEXT NOT NULL,
|
|
292
|
+
session_json TEXT NOT NULL,
|
|
293
|
+
updated_at_ms INTEGER NOT NULL
|
|
294
|
+
)
|
|
295
|
+
`);
|
|
296
|
+
const loadStatement = prepareSqliteStatement(options.db, `SELECT session_json FROM ${tableName} WHERE session_key = ?`);
|
|
297
|
+
const saveStatement = prepareSqliteStatement(options.db, `
|
|
298
|
+
INSERT INTO ${tableName} (session_key, app_name, user_id, session_id, session_json, updated_at_ms)
|
|
299
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
300
|
+
ON CONFLICT(session_key) DO UPDATE SET
|
|
301
|
+
app_name = excluded.app_name,
|
|
302
|
+
user_id = excluded.user_id,
|
|
303
|
+
session_id = excluded.session_id,
|
|
304
|
+
session_json = excluded.session_json,
|
|
305
|
+
updated_at_ms = excluded.updated_at_ms
|
|
306
|
+
`);
|
|
307
|
+
const updateCasStatement = prepareSqliteStatement(options.db, `
|
|
308
|
+
UPDATE ${tableName}
|
|
309
|
+
SET app_name = ?,
|
|
310
|
+
user_id = ?,
|
|
311
|
+
session_id = ?,
|
|
312
|
+
session_json = ?,
|
|
313
|
+
updated_at_ms = ?
|
|
314
|
+
WHERE session_key = ?
|
|
315
|
+
AND updated_at_ms = ?
|
|
316
|
+
`);
|
|
317
|
+
const save = (session, saveOptions) => {
|
|
318
|
+
const existing = load(session);
|
|
319
|
+
assertExpectedRevision(existing, saveOptions?.expectedRevision, "AgentSession");
|
|
320
|
+
const next = cloneSession({ ...session, updatedAt: Date.now() });
|
|
321
|
+
next.revision = existing ? existing.revision + 1 : next.revision;
|
|
322
|
+
if (saveOptions?.expectedRevision !== undefined && existing) {
|
|
323
|
+
const result = updateCasStatement.run([
|
|
324
|
+
next.appName,
|
|
325
|
+
next.userId,
|
|
326
|
+
next.sessionId,
|
|
327
|
+
JSON.stringify(next),
|
|
328
|
+
next.updatedAt,
|
|
329
|
+
sessionKey(next),
|
|
330
|
+
existing.updatedAt
|
|
331
|
+
]);
|
|
332
|
+
if (sqliteMutationCount(result) === 0) {
|
|
333
|
+
throw new ConflictError("AgentSession revision conflict.");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
saveStatement.run([
|
|
338
|
+
sessionKey(next),
|
|
339
|
+
next.appName,
|
|
340
|
+
next.userId,
|
|
341
|
+
next.sessionId,
|
|
342
|
+
JSON.stringify(next),
|
|
343
|
+
next.updatedAt
|
|
344
|
+
]);
|
|
345
|
+
}
|
|
346
|
+
return next;
|
|
347
|
+
};
|
|
348
|
+
const load = (input) => {
|
|
349
|
+
const row = loadStatement.get([sessionKey(input)]);
|
|
350
|
+
return parseSessionJson(getRecordField(row, ["session_json", "sessionJson"]));
|
|
351
|
+
};
|
|
352
|
+
return {
|
|
353
|
+
loadSession(input) {
|
|
354
|
+
return load(input);
|
|
355
|
+
},
|
|
356
|
+
createSession(input) {
|
|
357
|
+
return save(createEmptySession(input));
|
|
358
|
+
},
|
|
359
|
+
saveSession(session, options) {
|
|
360
|
+
save(session, options);
|
|
361
|
+
},
|
|
362
|
+
appendEvent(input) {
|
|
363
|
+
const existing = load(input);
|
|
364
|
+
assertExpectedRevision(existing, input.expectedRevision, "AgentSession");
|
|
365
|
+
const base = existing ?? createEmptySession(input);
|
|
366
|
+
return save(appendEventToSession(base, input.event), input.expectedRevision === undefined ? undefined : { expectedRevision: input.expectedRevision });
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
export const createPostgresSessionService = (options) => {
|
|
371
|
+
const tableName = validateIdentifier(options.tableName ?? "zhivex_agent_sessions", "tableName");
|
|
372
|
+
const createSql = `
|
|
373
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
374
|
+
session_key TEXT PRIMARY KEY,
|
|
375
|
+
app_name TEXT NOT NULL,
|
|
376
|
+
user_id TEXT NOT NULL,
|
|
377
|
+
session_id TEXT NOT NULL,
|
|
378
|
+
session_json JSONB NOT NULL,
|
|
379
|
+
updated_at_ms BIGINT NOT NULL
|
|
380
|
+
)
|
|
381
|
+
`;
|
|
382
|
+
const load = async (input) => {
|
|
383
|
+
await ensurePostgresTable(options.client, tableName, createSql);
|
|
384
|
+
const result = await options.client.query(`SELECT session_json FROM ${tableName} WHERE session_key = $1`, [sessionKey(input)]);
|
|
385
|
+
return parseSessionJson(getRecordField(result.rows[0], ["session_json", "sessionJson"]));
|
|
386
|
+
};
|
|
387
|
+
const save = async (session, saveOptions) => {
|
|
388
|
+
await ensurePostgresTable(options.client, tableName, createSql);
|
|
389
|
+
const existing = await load(session);
|
|
390
|
+
assertExpectedRevision(existing, saveOptions?.expectedRevision, "AgentSession");
|
|
391
|
+
const next = cloneSession({ ...session, updatedAt: Date.now() });
|
|
392
|
+
next.revision = existing ? existing.revision + 1 : next.revision;
|
|
393
|
+
if (saveOptions?.expectedRevision !== undefined && existing) {
|
|
394
|
+
const result = await options.client.query(`UPDATE ${tableName}
|
|
395
|
+
SET app_name = $2,
|
|
396
|
+
user_id = $3,
|
|
397
|
+
session_id = $4,
|
|
398
|
+
session_json = $5::jsonb,
|
|
399
|
+
updated_at_ms = $6
|
|
400
|
+
WHERE session_key = $1
|
|
401
|
+
AND updated_at_ms = $7
|
|
402
|
+
RETURNING session_json`, [sessionKey(next), next.appName, next.userId, next.sessionId, JSON.stringify(next), next.updatedAt, existing.updatedAt]);
|
|
403
|
+
if (result.rows.length === 0) {
|
|
404
|
+
throw new ConflictError("AgentSession revision conflict.");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
await options.client.query(`INSERT INTO ${tableName} (session_key, app_name, user_id, session_id, session_json, updated_at_ms)
|
|
409
|
+
VALUES ($1, $2, $3, $4, $5::jsonb, $6)
|
|
410
|
+
ON CONFLICT(session_key) DO UPDATE SET
|
|
411
|
+
app_name = EXCLUDED.app_name,
|
|
412
|
+
user_id = EXCLUDED.user_id,
|
|
413
|
+
session_id = EXCLUDED.session_id,
|
|
414
|
+
session_json = EXCLUDED.session_json,
|
|
415
|
+
updated_at_ms = EXCLUDED.updated_at_ms`, [sessionKey(next), next.appName, next.userId, next.sessionId, JSON.stringify(next), next.updatedAt]);
|
|
416
|
+
}
|
|
417
|
+
return next;
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
loadSession(input) {
|
|
421
|
+
return load(input);
|
|
422
|
+
},
|
|
423
|
+
createSession(input) {
|
|
424
|
+
return save(createEmptySession(input));
|
|
425
|
+
},
|
|
426
|
+
async saveSession(session, options) {
|
|
427
|
+
await save(session, options);
|
|
428
|
+
},
|
|
429
|
+
async appendEvent(input) {
|
|
430
|
+
const existing = await load(input);
|
|
431
|
+
assertExpectedRevision(existing, input.expectedRevision, "AgentSession");
|
|
432
|
+
const base = existing ?? createEmptySession(input);
|
|
433
|
+
return save(appendEventToSession(base, input.event), input.expectedRevision === undefined ? undefined : { expectedRevision: input.expectedRevision });
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
};
|
|
437
|
+
const loadOrCreateSession = async (sessionService, input) => {
|
|
438
|
+
const loaded = await sessionService.loadSession(input);
|
|
439
|
+
if (loaded) {
|
|
440
|
+
return loaded;
|
|
441
|
+
}
|
|
442
|
+
await sessionService.createSession(input);
|
|
443
|
+
const event = toSessionEvent("session-created", {
|
|
444
|
+
appName: input.appName,
|
|
445
|
+
userId: input.userId,
|
|
446
|
+
sessionId: input.sessionId,
|
|
447
|
+
metadata: input.metadata
|
|
448
|
+
});
|
|
449
|
+
return sessionService.appendEvent({ ...input, event });
|
|
450
|
+
};
|
|
451
|
+
const messagesFromSession = (session) => session.events.flatMap((event) => {
|
|
452
|
+
if (event.type === "user-message") {
|
|
453
|
+
return event.messages ? cloneJson(event.messages) : [];
|
|
454
|
+
}
|
|
455
|
+
if (event.type === "agent-run-finished" && event.outputText) {
|
|
456
|
+
return [createTextMessage("assistant", event.outputText)];
|
|
457
|
+
}
|
|
458
|
+
return [];
|
|
459
|
+
});
|
|
460
|
+
const messagesFromInput = (input) => normalizeMessages({
|
|
461
|
+
prompt: input.prompt,
|
|
462
|
+
messages: input.messages,
|
|
463
|
+
system: input.system
|
|
464
|
+
});
|
|
465
|
+
const appendEvent = async (service, session, event) => service.appendEvent({
|
|
466
|
+
appName: session.appName,
|
|
467
|
+
userId: session.userId,
|
|
468
|
+
sessionId: session.sessionId,
|
|
469
|
+
event
|
|
470
|
+
});
|
|
471
|
+
const saveRunState = async (service, session, state) => {
|
|
472
|
+
const next = {
|
|
473
|
+
...session,
|
|
474
|
+
updatedAt: Date.now(),
|
|
475
|
+
lastRunState: cloneJson(state)
|
|
476
|
+
};
|
|
477
|
+
await service.saveSession(next);
|
|
478
|
+
return cloneSession(next);
|
|
479
|
+
};
|
|
480
|
+
const createRunStartedEvent = (session, metadata) => toSessionEvent("agent-run-started", {
|
|
481
|
+
appName: session.appName,
|
|
482
|
+
userId: session.userId,
|
|
483
|
+
sessionId: session.sessionId,
|
|
484
|
+
metadata
|
|
485
|
+
});
|
|
486
|
+
const createRunFinishedEvent = (session, output, metadata) => toSessionEvent("agent-run-finished", {
|
|
487
|
+
appName: session.appName,
|
|
488
|
+
userId: session.userId,
|
|
489
|
+
sessionId: session.sessionId,
|
|
490
|
+
runId: output.state.runId,
|
|
491
|
+
status: output.status,
|
|
492
|
+
outputText: output.steps.at(-1)?.response?.text ?? output.outputText,
|
|
493
|
+
metadata
|
|
494
|
+
});
|
|
495
|
+
const createRunFailedEvent = (session, error, metadata) => toSessionEvent("agent-run-failed", {
|
|
496
|
+
appName: session.appName,
|
|
497
|
+
userId: session.userId,
|
|
498
|
+
sessionId: session.sessionId,
|
|
499
|
+
error: {
|
|
500
|
+
message: error instanceof Error ? error.message : String(error)
|
|
501
|
+
},
|
|
502
|
+
metadata
|
|
503
|
+
});
|
|
504
|
+
const createApprovalEvent = (session, output, metadata) => {
|
|
505
|
+
if (!output.state.pendingApprovals.length) {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
return toSessionEvent("approval-required", {
|
|
509
|
+
appName: session.appName,
|
|
510
|
+
userId: session.userId,
|
|
511
|
+
sessionId: session.sessionId,
|
|
512
|
+
runId: output.state.runId,
|
|
513
|
+
approvals: cloneJson(output.state.pendingApprovals),
|
|
514
|
+
metadata
|
|
515
|
+
});
|
|
516
|
+
};
|
|
517
|
+
const toSerializableMetadata = (metadata) => metadata ? serializeJsonValue(metadata) : undefined;
|
|
518
|
+
export const createRunner = (options) => {
|
|
519
|
+
const resolveSession = async (input) => loadOrCreateSession(options.sessionService, {
|
|
520
|
+
appName: options.appName,
|
|
521
|
+
userId: input.userId,
|
|
522
|
+
sessionId: input.sessionId ?? randomId("sess"),
|
|
523
|
+
metadata: toSerializableMetadata(input.sessionMetadata)
|
|
524
|
+
});
|
|
525
|
+
const createAgentInput = (session, input) => {
|
|
526
|
+
const { userId: _userId, sessionId: _sessionId, sessionMetadata: _sessionMetadata, eventMetadata: _eventMetadata, ...agentInput } = input;
|
|
527
|
+
const messages = messagesFromInput(input);
|
|
528
|
+
if (input.approvals?.length && session.lastRunState) {
|
|
529
|
+
return {
|
|
530
|
+
...options.defaults,
|
|
531
|
+
...agentInput,
|
|
532
|
+
state: session.lastRunState,
|
|
533
|
+
approvals: input.approvals
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
...options.defaults,
|
|
538
|
+
...agentInput,
|
|
539
|
+
prompt: undefined,
|
|
540
|
+
system: undefined,
|
|
541
|
+
messages: [...messagesFromSession(session), ...messages]
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
const recordUserMessage = async (session, input) => {
|
|
545
|
+
if (input.approvals?.length && !input.prompt && !input.messages?.length) {
|
|
546
|
+
return session;
|
|
547
|
+
}
|
|
548
|
+
const messages = messagesFromInput(input).filter((message) => message.role !== "system");
|
|
549
|
+
if (!messages.length) {
|
|
550
|
+
return session;
|
|
551
|
+
}
|
|
552
|
+
return appendEvent(options.sessionService, session, toSessionEvent("user-message", {
|
|
553
|
+
appName: session.appName,
|
|
554
|
+
userId: session.userId,
|
|
555
|
+
sessionId: session.sessionId,
|
|
556
|
+
messages,
|
|
557
|
+
metadata: toSerializableMetadata(input.eventMetadata)
|
|
558
|
+
}));
|
|
559
|
+
};
|
|
560
|
+
const finalizeSuccessfulRun = async (session, output, metadata) => {
|
|
561
|
+
let next = await appendEvent(options.sessionService, session, createRunFinishedEvent(session, output, metadata));
|
|
562
|
+
const approvalEvent = createApprovalEvent(next, output, metadata);
|
|
563
|
+
if (approvalEvent) {
|
|
564
|
+
next = await appendEvent(options.sessionService, next, approvalEvent);
|
|
565
|
+
}
|
|
566
|
+
return saveRunState(options.sessionService, next, output.state);
|
|
567
|
+
};
|
|
568
|
+
return {
|
|
569
|
+
async run(input) {
|
|
570
|
+
const initialSession = await resolveSession(input);
|
|
571
|
+
let session = await recordUserMessage(initialSession, input);
|
|
572
|
+
session = await appendEvent(options.sessionService, session, createRunStartedEvent(session, toSerializableMetadata(input.eventMetadata)));
|
|
573
|
+
try {
|
|
574
|
+
const agentInput = createAgentInput(initialSession, input);
|
|
575
|
+
const output = input.approvals?.length && initialSession.lastRunState
|
|
576
|
+
? await resumeAgent(options.agent, agentInput)
|
|
577
|
+
: await runAgent(options.agent, agentInput);
|
|
578
|
+
const finalSession = await finalizeSuccessfulRun(session, output, toSerializableMetadata(input.eventMetadata));
|
|
579
|
+
return {
|
|
580
|
+
session: finalSession,
|
|
581
|
+
output
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
const failedSession = await appendEvent(options.sessionService, session, createRunFailedEvent(session, error, toSerializableMetadata(input.eventMetadata)));
|
|
586
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), {
|
|
587
|
+
session: failedSession
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
stream(input) {
|
|
592
|
+
const setup = (async () => {
|
|
593
|
+
const initialSession = await resolveSession(input);
|
|
594
|
+
let session = await recordUserMessage(initialSession, input);
|
|
595
|
+
session = await appendEvent(options.sessionService, session, createRunStartedEvent(session, toSerializableMetadata(input.eventMetadata)));
|
|
596
|
+
const agentInput = createAgentInput(initialSession, input);
|
|
597
|
+
const stream = input.approvals?.length && initialSession.lastRunState
|
|
598
|
+
? streamAgent(options.agent, agentInput)
|
|
599
|
+
: streamAgent(options.agent, agentInput);
|
|
600
|
+
return { session, stream };
|
|
601
|
+
})();
|
|
602
|
+
const collect = async () => {
|
|
603
|
+
const { session, stream } = await setup;
|
|
604
|
+
try {
|
|
605
|
+
const output = await stream.collect();
|
|
606
|
+
const finalSession = await finalizeSuccessfulRun(session, output, toSerializableMetadata(input.eventMetadata));
|
|
607
|
+
return { session: finalSession, output };
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
const failedSession = await appendEvent(options.sessionService, session, createRunFailedEvent(session, error, toSerializableMetadata(input.eventMetadata)));
|
|
611
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), {
|
|
612
|
+
session: failedSession
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
return {
|
|
617
|
+
session: setup.then(({ session }) => session),
|
|
618
|
+
eventStream: (async function* () {
|
|
619
|
+
const { stream } = await setup;
|
|
620
|
+
for await (const event of stream.eventStream) {
|
|
621
|
+
yield event;
|
|
622
|
+
}
|
|
623
|
+
})(),
|
|
624
|
+
textStream: (async function* () {
|
|
625
|
+
const { stream } = await setup;
|
|
626
|
+
for await (const chunk of stream.textStream) {
|
|
627
|
+
yield chunk;
|
|
628
|
+
}
|
|
629
|
+
})(),
|
|
630
|
+
collect
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
};
|
|
635
|
+
//# sourceMappingURL=runner.js.map
|