@purista/harness 1.2.6 → 1.5.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 +6 -0
- package/dist/agents/index.d.ts +7 -1
- package/dist/agents/index.js +56 -38
- package/dist/errors/catalog.d.ts +18 -2
- package/dist/errors/catalog.js +10 -0
- package/dist/eval/index.d.ts +3 -3
- package/dist/eval/index.js +15 -1
- package/dist/harness/defineHarness.d.ts +91 -1
- package/dist/harness/defineHarness.js +110 -1
- package/dist/index.d.ts +37 -17
- package/dist/index.js +30 -16
- package/dist/local/index.d.ts +36 -0
- package/dist/local/index.js +24 -0
- package/dist/local/local-sandbox.d.ts +25 -0
- package/dist/local/local-sandbox.js +368 -0
- package/dist/local/local-workspace.d.ts +56 -0
- package/dist/local/local-workspace.js +496 -0
- package/dist/local/ref-hash.d.ts +6 -0
- package/dist/local/ref-hash.js +9 -0
- package/dist/local/sqlite-storage.d.ts +106 -0
- package/dist/local/sqlite-storage.js +680 -0
- package/dist/models/adapter-utils.d.ts +52 -0
- package/dist/models/adapter-utils.js +81 -0
- package/dist/models/registry.js +28 -37
- package/dist/models/stream-pump.d.ts +16 -0
- package/dist/models/stream-pump.js +77 -0
- package/dist/ports/base-model-provider.d.ts +7 -1
- package/dist/ports/base-model-provider.js +384 -87
- package/dist/ports/capabilities.d.ts +16 -2
- package/dist/ports/context-checkpoints.d.ts +63 -0
- package/dist/ports/context-checkpoints.js +33 -0
- package/dist/ports/index.d.ts +1 -0
- package/dist/ports/index.js +1 -0
- package/dist/ports/model-provider.d.ts +94 -0
- package/dist/runtime/durable.d.ts +11 -0
- package/dist/runtime/durable.js +15 -2
- package/dist/runtime/sessionDurable.js +47 -21
- package/dist/sessions/index.d.ts +17 -6
- package/dist/sessions/index.js +337 -81
- package/dist/skills/index.d.ts +0 -2
- package/dist/skills/index.js +0 -8
- package/dist/state/in-memory.js +6 -6
- package/dist/telemetry/shim.js +2 -6
- package/dist/telemetry/span-attrs.d.ts +9 -0
- package/dist/telemetry/span-attrs.js +27 -0
- package/dist/testing/durableWorkspaceStoreContract.js +69 -0
- package/dist/testing/fakeLogger.d.ts +29 -0
- package/dist/testing/fakeLogger.js +47 -0
- package/dist/testing/fakeSandbox.d.ts +27 -0
- package/dist/testing/fakeSandbox.js +153 -0
- package/dist/testing/fakeStateStore.d.ts +36 -0
- package/dist/testing/fakeStateStore.js +66 -0
- package/dist/testing/index.d.ts +10 -4
- package/dist/testing/index.js +14 -4
- package/dist/testing/loggerContract.d.ts +9 -0
- package/dist/testing/loggerContract.js +62 -0
- package/dist/testing/modelProviderContract.d.ts +12 -0
- package/dist/testing/modelProviderContract.js +222 -0
- package/dist/testing/recordEvents.d.ts +3 -0
- package/dist/testing/recordEvents.js +8 -0
- package/dist/testing/stateStoreContract.js +27 -0
- package/dist/tools/index.js +26 -1
- package/dist/tools/mcp/http.d.ts +2 -0
- package/dist/tools/mcp/http.js +34 -21
- package/dist/tools/mcp/runner.d.ts +4 -0
- package/dist/tools/mcp/runner.js +75 -21
- package/dist/tools/mcp/stdio.d.ts +7 -1
- package/dist/tools/mcp/stdio.js +102 -23
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace/in-memory.d.ts +1 -0
- package/dist/workspace/in-memory.js +47 -12
- package/package.json +2 -1
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { HarnessConfigError, OperationCancelledError, StateError, WorkspaceError } from '../errors/index.js';
|
|
5
|
+
import { AsyncMutex, DurableRunLeaseError, DurableTerminalRunError, isResumeBlockingRunStatus } from '../runtime/durable.js';
|
|
6
|
+
import { DurableStepError } from '../runtime/steps.js';
|
|
7
|
+
import { sha256Hex } from './ref-hash.js';
|
|
8
|
+
const SQLITE_ENGINE_REQUIREMENT = 'node>=24.15.0 (node:sqlite) or bun (bun:sqlite)';
|
|
9
|
+
class BuiltinSqliteStatement {
|
|
10
|
+
statement;
|
|
11
|
+
constructor(statement) {
|
|
12
|
+
this.statement = statement;
|
|
13
|
+
}
|
|
14
|
+
get(...params) {
|
|
15
|
+
const row = this.statement.get(...params);
|
|
16
|
+
return row && typeof row === 'object' ? row : undefined;
|
|
17
|
+
}
|
|
18
|
+
all(...params) {
|
|
19
|
+
return this.statement.all(...params).filter((row) => Boolean(row && typeof row === 'object'));
|
|
20
|
+
}
|
|
21
|
+
run(...params) {
|
|
22
|
+
this.statement.run(...params);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function openBuiltinSqlite(file) {
|
|
26
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const versions = globalThis.process?.versions;
|
|
29
|
+
const runtime = versions?.['bun'] ? 'bun' : 'node';
|
|
30
|
+
const moduleName = runtime === 'bun' ? 'bun:sqlite' : 'node:sqlite';
|
|
31
|
+
let loaded;
|
|
32
|
+
try {
|
|
33
|
+
loaded = require(moduleName);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new HarnessConfigError(`Built-in SQLite driver is unavailable. Requires ${SQLITE_ENGINE_REQUIREMENT}.`, {
|
|
37
|
+
reason: 'sqlite_unavailable',
|
|
38
|
+
path: 'localDurableExecution.databaseFile',
|
|
39
|
+
id: runtime
|
|
40
|
+
}, error);
|
|
41
|
+
}
|
|
42
|
+
const Database = loaded.DatabaseSync ?? loaded.Database;
|
|
43
|
+
if (!Database) {
|
|
44
|
+
throw new HarnessConfigError(`Built-in SQLite driver is unavailable. Requires ${SQLITE_ENGINE_REQUIREMENT}.`, {
|
|
45
|
+
reason: 'sqlite_unavailable',
|
|
46
|
+
path: 'localDurableExecution.databaseFile',
|
|
47
|
+
id: runtime
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const raw = new Database(file);
|
|
51
|
+
return {
|
|
52
|
+
exec: (sql) => raw.exec(sql),
|
|
53
|
+
prepare: (sql) => new BuiltinSqliteStatement(raw.prepare(sql)),
|
|
54
|
+
close: () => raw.close()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function stringify(value) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return null;
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
}
|
|
62
|
+
function parseJson(value) {
|
|
63
|
+
if (typeof value !== 'string')
|
|
64
|
+
return undefined;
|
|
65
|
+
return JSON.parse(value);
|
|
66
|
+
}
|
|
67
|
+
function contextRefHash(ref) {
|
|
68
|
+
return sha256Hex(`${ref.runId}:${ref.sessionId}:${ref.sequence}:${ref.kind}`);
|
|
69
|
+
}
|
|
70
|
+
function isConstraintViolation(error) {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
return /constraint|unique/i.test(message);
|
|
73
|
+
}
|
|
74
|
+
function requiredString(row, key, op) {
|
|
75
|
+
const value = row[key];
|
|
76
|
+
if (typeof value !== 'string')
|
|
77
|
+
throw new StateError('SQLite row is missing a required string.', { op, reason: key });
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function requiredNumber(row, key, op) {
|
|
81
|
+
const value = row[key];
|
|
82
|
+
if (typeof value !== 'number')
|
|
83
|
+
throw new StateError('SQLite row is missing a required number.', { op, reason: key });
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
/** SQLite-backed local storage implementing StateStore, DurableRuntime, and ContextCheckpointStore. */
|
|
87
|
+
export class SqliteHarnessStorage {
|
|
88
|
+
capabilities = [
|
|
89
|
+
'runtime.checkpoint',
|
|
90
|
+
'runtime.retry',
|
|
91
|
+
'runtime.distributed_lock',
|
|
92
|
+
'runtime.resume_from_checkpoint',
|
|
93
|
+
'runtime.workspace_checkpoint',
|
|
94
|
+
'runtime.persistent',
|
|
95
|
+
'context_checkpoint.write',
|
|
96
|
+
'context_checkpoint.read',
|
|
97
|
+
'context_checkpoint.list',
|
|
98
|
+
'context_checkpoint.delete',
|
|
99
|
+
'context_checkpoint.persistent'
|
|
100
|
+
];
|
|
101
|
+
id = 'sqlite_runtime';
|
|
102
|
+
info = {
|
|
103
|
+
id: 'sqlite_context_checkpoints',
|
|
104
|
+
packageName: '@purista/harness',
|
|
105
|
+
capabilities: [
|
|
106
|
+
'context_checkpoint.write',
|
|
107
|
+
'context_checkpoint.read',
|
|
108
|
+
'context_checkpoint.list',
|
|
109
|
+
'context_checkpoint.delete',
|
|
110
|
+
'context_checkpoint.persistent'
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
db;
|
|
114
|
+
leaseTtlMs;
|
|
115
|
+
clock;
|
|
116
|
+
/**
|
|
117
|
+
* In-process serialization for SQLite transactions: one connection allows a
|
|
118
|
+
* single open transaction, so every transactional entry point goes through
|
|
119
|
+
* this mutex before issuing `begin immediate`.
|
|
120
|
+
*/
|
|
121
|
+
dbLock = new AsyncMutex();
|
|
122
|
+
sessionLocks = new Map();
|
|
123
|
+
statements = new Map();
|
|
124
|
+
closed = false;
|
|
125
|
+
logger;
|
|
126
|
+
telemetry;
|
|
127
|
+
constructor(options) {
|
|
128
|
+
this.leaseTtlMs = options.leaseTtlMs ?? 120_000;
|
|
129
|
+
this.clock = options.now ?? Date.now;
|
|
130
|
+
this.db = openBuiltinSqlite(options.file);
|
|
131
|
+
this.migrate();
|
|
132
|
+
}
|
|
133
|
+
configureHarnessContext(context) {
|
|
134
|
+
this.logger = context.logger;
|
|
135
|
+
this.telemetry = context.telemetry;
|
|
136
|
+
}
|
|
137
|
+
async getSession(id) {
|
|
138
|
+
const row = this.stmt('select * from harness_sessions where id = ?').get(id);
|
|
139
|
+
return row ? this.rowToSession(row) : undefined;
|
|
140
|
+
}
|
|
141
|
+
async upsertSession(record) {
|
|
142
|
+
this.stmt('insert into harness_sessions(id, created_at, updated_at, run_count, metadata_json) values(?, ?, ?, ?, ?) on conflict(id) do update set updated_at=excluded.updated_at, run_count=excluded.run_count, metadata_json=excluded.metadata_json')
|
|
143
|
+
.run(record.id, record.createdAt, record.updatedAt, record.runCount, stringify(record.metadata));
|
|
144
|
+
}
|
|
145
|
+
async closeSession(id) {
|
|
146
|
+
await this.transaction(() => {
|
|
147
|
+
this.stmt('delete from harness_sessions where id = ?').run(id);
|
|
148
|
+
this.stmt('delete from harness_messages where session_id = ?').run(id);
|
|
149
|
+
this.stmt('delete from harness_run_events where run_id in (select id from harness_runs where session_id = ?)').run(id);
|
|
150
|
+
this.stmt('delete from harness_runs where session_id = ?').run(id);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async appendMessages(sessionId, messages) {
|
|
154
|
+
await this.transaction(() => {
|
|
155
|
+
const insert = this.stmt('insert into harness_messages(id, session_id, role, content, tool_calls_json, tool_results_json, timestamp) values(?, ?, ?, ?, ?, ?, ?)');
|
|
156
|
+
for (const message of messages) {
|
|
157
|
+
try {
|
|
158
|
+
insert.run(message.id, sessionId, message.role, message.content, stringify(message.toolCalls), stringify(message.toolResults), message.timestamp);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (isConstraintViolation(error)) {
|
|
162
|
+
throw new StateError('Message id already exists.', { op: 'appendMessages', reason: 'duplicate_message_id' }, error);
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async listMessages(sessionId, opts = {}) {
|
|
170
|
+
const before = opts.before ? this.stmt('select timestamp, id from harness_messages where id = ? and session_id = ?').get(opts.before, sessionId) : undefined;
|
|
171
|
+
const beforeClause = before ? ' and (timestamp < ? or (timestamp = ? and id < ?))' : '';
|
|
172
|
+
const beforeParams = before
|
|
173
|
+
? [requiredString(before, 'timestamp', 'listMessages'), requiredString(before, 'timestamp', 'listMessages'), opts.before ?? '']
|
|
174
|
+
: [];
|
|
175
|
+
if (opts.limit === undefined) {
|
|
176
|
+
const rows = this.stmt(`select * from harness_messages where session_id = ?${beforeClause} order by timestamp asc, id asc`).all(sessionId, ...beforeParams);
|
|
177
|
+
return rows.map((row) => this.rowToMessage(row));
|
|
178
|
+
}
|
|
179
|
+
// Tail semantics: fetch the newest `limit` rows and restore ascending order.
|
|
180
|
+
const rows = this.stmt(`select * from harness_messages where session_id = ?${beforeClause} order by timestamp desc, id desc limit ?`).all(sessionId, ...beforeParams, Math.max(0, opts.limit));
|
|
181
|
+
return rows.reverse().map((row) => this.rowToMessage(row));
|
|
182
|
+
}
|
|
183
|
+
async clearMessages(sessionId) {
|
|
184
|
+
this.stmt('delete from harness_messages where session_id = ?').run(sessionId);
|
|
185
|
+
}
|
|
186
|
+
async replaceMessages(sessionId, messages) {
|
|
187
|
+
await this.transaction(() => {
|
|
188
|
+
this.stmt('delete from harness_messages where session_id = ?').run(sessionId);
|
|
189
|
+
const insert = this.stmt('insert into harness_messages(id, session_id, role, content, tool_calls_json, tool_results_json, timestamp) values(?, ?, ?, ?, ?, ?, ?)');
|
|
190
|
+
for (const message of messages) {
|
|
191
|
+
try {
|
|
192
|
+
insert.run(message.id, sessionId, message.role, message.content, stringify(message.toolCalls), stringify(message.toolResults), message.timestamp);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (isConstraintViolation(error)) {
|
|
196
|
+
throw new StateError('Message id already exists.', { op: 'replaceMessages', reason: 'duplicate_message_id' }, error);
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async createRun(record) {
|
|
204
|
+
await this.transaction(() => {
|
|
205
|
+
const existing = this.loadRun(record.id);
|
|
206
|
+
if (existing) {
|
|
207
|
+
if (existing.status === 'succeeded' || existing.status === 'cancelled') {
|
|
208
|
+
throw new StateError('Terminal run already exists.', { op: 'createRun', reason: 'terminal_run_exists' });
|
|
209
|
+
}
|
|
210
|
+
if (existing.sessionId === record.sessionId && existing.kind === record.kind && existing.target === record.target) {
|
|
211
|
+
this.stmt('update harness_runs set status = ?, started_at = ?, finished_at = null, input_json = ?, output_json = null, error_json = null where id = ?')
|
|
212
|
+
.run('running', record.startedAt, stringify(record.input), record.id);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
throw new StateError('Run id already exists for a different run.', { op: 'createRun', reason: 'run_conflict' });
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
this.stmt('insert into harness_runs(id, session_id, kind, target, started_at, finished_at, status, input_json, output_json, error_json) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
219
|
+
.run(record.id, record.sessionId, record.kind, record.target, record.startedAt, record.finishedAt ?? null, record.status, stringify(record.input), stringify(record.output), stringify(record.error));
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (isConstraintViolation(error)) {
|
|
223
|
+
throw new StateError('Run id already exists for a different run.', { op: 'createRun', reason: 'run_conflict' }, error);
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
async finishRun(runId, patch) {
|
|
230
|
+
return this.runtimeSpan('finish', {
|
|
231
|
+
'harness.run.id': runId,
|
|
232
|
+
'harness.run.status': patch.status
|
|
233
|
+
}, async () => this.transaction(() => {
|
|
234
|
+
this.stmt('update harness_runs set status = coalesce(?, status), finished_at = coalesce(?, finished_at), output_json = coalesce(?, output_json), error_json = coalesce(?, error_json) where id = ?')
|
|
235
|
+
.run(patch.status ?? null, patch.finishedAt ?? null, stringify(patch.output), stringify(patch.error), runId);
|
|
236
|
+
if (patch.status) {
|
|
237
|
+
// Spec 22 §3: every terminal status (including `failed`) is recorded on
|
|
238
|
+
// the durable run with its sanitized error and releases the lease.
|
|
239
|
+
// Only `succeeded`/`cancelled` block a later resume.
|
|
240
|
+
this.stmt('update harness_durable_runs set status = ?, output_json = ?, error_json = ?, finished_at = ? where run_id = ?')
|
|
241
|
+
.run(patch.status, stringify(patch.output), stringify(patch.error), patch.finishedAt ?? this.nowIso(), runId);
|
|
242
|
+
this.stmt('delete from harness_durable_leases where run_id = ?').run(runId);
|
|
243
|
+
}
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
async getRun(runId) {
|
|
247
|
+
return this.loadRun(runId);
|
|
248
|
+
}
|
|
249
|
+
async listRuns(sessionId, opts = {}) {
|
|
250
|
+
const before = opts.before ? this.stmt('select started_at, id from harness_runs where id = ? and session_id = ?').get(opts.before, sessionId) : undefined;
|
|
251
|
+
const beforeClause = before ? ' and (started_at < ? or (started_at = ? and id < ?))' : '';
|
|
252
|
+
const beforeParams = before
|
|
253
|
+
? [requiredString(before, 'started_at', 'listRuns'), requiredString(before, 'started_at', 'listRuns'), opts.before ?? '']
|
|
254
|
+
: [];
|
|
255
|
+
const limitClause = opts.limit === undefined ? '' : ' limit ?';
|
|
256
|
+
const limitParams = opts.limit === undefined ? [] : [Math.max(0, opts.limit)];
|
|
257
|
+
const rows = this.stmt(`select * from harness_runs where session_id = ?${beforeClause} order by started_at desc, id desc${limitClause}`).all(sessionId, ...beforeParams, ...limitParams);
|
|
258
|
+
return rows.map((row) => this.rowToRun(row));
|
|
259
|
+
}
|
|
260
|
+
async appendEvents(runId, events) {
|
|
261
|
+
await this.transaction(() => {
|
|
262
|
+
const insert = this.stmt('insert into harness_run_events(id, run_id, at, type, payload_json) values(?, ?, ?, ?, ?)');
|
|
263
|
+
for (const event of events)
|
|
264
|
+
insert.run(event.id, runId, event.at, event.type, JSON.stringify(event.payload));
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async listEvents(runId, opts = {}) {
|
|
268
|
+
const afterClause = opts.after ? ' and id > ?' : '';
|
|
269
|
+
const afterParams = opts.after ? [opts.after] : [];
|
|
270
|
+
const limitClause = opts.limit === undefined ? '' : ' limit ?';
|
|
271
|
+
const limitParams = opts.limit === undefined ? [] : [Math.max(0, opts.limit)];
|
|
272
|
+
const rows = this.stmt(`select * from harness_run_events where run_id = ?${afterClause} order by id asc${limitClause}`).all(runId, ...afterParams, ...limitParams);
|
|
273
|
+
return rows.map((row) => ({
|
|
274
|
+
id: requiredString(row, 'id', 'listEvents'),
|
|
275
|
+
runId: requiredString(row, 'run_id', 'listEvents'),
|
|
276
|
+
at: requiredString(row, 'at', 'listEvents'),
|
|
277
|
+
type: requiredString(row, 'type', 'listEvents'),
|
|
278
|
+
payload: parseJson(row['payload_json']) ?? null
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
async startRun(record) {
|
|
282
|
+
return this.runtimeSpan('start', {
|
|
283
|
+
'harness.run.id': record.runId,
|
|
284
|
+
'harness.session.id': record.sessionId
|
|
285
|
+
}, (recordAttrs) => this.withSessionLock(record.sessionId, async () => this.transaction(() => {
|
|
286
|
+
const current = this.loadDurableRun(record.runId);
|
|
287
|
+
// Failed runs stay resumable (spec 22 §3); only succeeded/cancelled block.
|
|
288
|
+
if (current && isResumeBlockingRunStatus(current.status)) {
|
|
289
|
+
throw new DurableTerminalRunError(record.runId, current.status);
|
|
290
|
+
}
|
|
291
|
+
this.assertLeaseAvailable(record.runId, record.sessionId, record.workerId);
|
|
292
|
+
const attempt = current ? current.attempt + 1 : Math.max(1, record.attempt ?? 1);
|
|
293
|
+
if (!current) {
|
|
294
|
+
this.stmt('insert into harness_durable_runs(run_id, session_id, worker_id, step_id, input_json, attempt, status, metadata_json, started_at) values(?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
295
|
+
.run(record.runId, record.sessionId, record.workerId, record.stepId, JSON.stringify(record.input), attempt, 'running', stringify(record.metadata), this.nowIso());
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this.stmt('update harness_durable_runs set attempt = ?, worker_id = ?, status = ?, finished_at = null, error_json = null where run_id = ?')
|
|
299
|
+
.run(attempt, record.workerId, 'running', record.runId);
|
|
300
|
+
}
|
|
301
|
+
const leaseId = `lease_${this.clock()}_${Math.random().toString(36).slice(2)}`;
|
|
302
|
+
const expiresAt = new Date(this.clock() + this.leaseTtlMs).toISOString();
|
|
303
|
+
// Upsert allows same-worker lease renewal for retries within the TTL.
|
|
304
|
+
this.stmt('insert into harness_durable_leases(run_id, session_id, worker_id, lease_id, expires_at) values(?, ?, ?, ?, ?) on conflict(run_id) do update set session_id=excluded.session_id, worker_id=excluded.worker_id, lease_id=excluded.lease_id, expires_at=excluded.expires_at')
|
|
305
|
+
.run(record.runId, record.sessionId, record.workerId, leaseId, expiresAt);
|
|
306
|
+
const lease = this.toLease(record.runId, leaseId);
|
|
307
|
+
recordAttrs({ 'harness.runtime.resumed': lease.resumed, 'harness.runtime.attempt': lease.attempt });
|
|
308
|
+
return lease;
|
|
309
|
+
})));
|
|
310
|
+
}
|
|
311
|
+
async loadCheckpoint(runId) {
|
|
312
|
+
return this.runtimeSpan('load_checkpoint', {
|
|
313
|
+
'harness.run.id': runId
|
|
314
|
+
}, async () => {
|
|
315
|
+
const row = this.stmt('select * from harness_durable_checkpoints where run_id = ? order by sequence desc limit 1').get(runId);
|
|
316
|
+
return row ? this.rowToCheckpoint(row) : undefined;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
async commitCheckpoint(checkpoint) {
|
|
320
|
+
return this.runtimeSpan('checkpoint', {
|
|
321
|
+
'harness.runtime.attempt': checkpoint.attempt,
|
|
322
|
+
'harness.runtime.sequence': checkpoint.sequence,
|
|
323
|
+
'harness.runtime.step_id': checkpoint.stepId,
|
|
324
|
+
'harness.run.id': checkpoint.runId,
|
|
325
|
+
'harness.session.id': checkpoint.sessionId
|
|
326
|
+
}, () => this.withSessionLock(checkpoint.sessionId, async () => {
|
|
327
|
+
// Serialize before any SQLite write so non-serializable payloads are
|
|
328
|
+
// rejected without mutating storage (spec 22 §3).
|
|
329
|
+
let inputJson;
|
|
330
|
+
let outputJson;
|
|
331
|
+
let replayJson;
|
|
332
|
+
let metadataJson;
|
|
333
|
+
try {
|
|
334
|
+
inputJson = JSON.stringify(checkpoint.input);
|
|
335
|
+
outputJson = stringify(checkpoint.output);
|
|
336
|
+
replayJson = stringify(checkpoint.replay);
|
|
337
|
+
metadataJson = stringify(checkpoint.metadata);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
throw new DurableStepError(`Durable checkpoint for step "${checkpoint.stepId}" is not JSON-serializable.`);
|
|
341
|
+
}
|
|
342
|
+
return this.transaction(() => {
|
|
343
|
+
const lease = this.stmt('select * from harness_durable_leases where run_id = ? and lease_id = ? and worker_id = ?').get(checkpoint.runId, checkpoint.leaseId, checkpoint.workerId);
|
|
344
|
+
if (!lease)
|
|
345
|
+
throw new DurableRunLeaseError(`Durable run "${checkpoint.runId}" is not owned by this lease.`);
|
|
346
|
+
// Heartbeat: each checkpoint by the owning lease renews the TTL so
|
|
347
|
+
// long runs are not taken over mid-flight.
|
|
348
|
+
this.stmt('update harness_durable_leases set expires_at = ? where run_id = ? and lease_id = ?')
|
|
349
|
+
.run(new Date(this.clock() + this.leaseTtlMs).toISOString(), checkpoint.runId, checkpoint.leaseId);
|
|
350
|
+
const existing = this.stmt('select * from harness_durable_checkpoints where run_id = ? and step_id = ?').get(checkpoint.runId, checkpoint.stepId);
|
|
351
|
+
if (existing) {
|
|
352
|
+
if (existing['output_json'] !== outputJson || existing['replay_json'] !== replayJson || existing['sequence'] !== checkpoint.sequence || existing['attempt'] !== checkpoint.attempt) {
|
|
353
|
+
throw new WorkspaceError('Durable checkpoint idempotency conflict.', { reason: 'checkpoint_conflict', run_id: checkpoint.runId, session_id: checkpoint.sessionId });
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.stmt('insert into harness_durable_checkpoints(run_id, session_id, lease_id, worker_id, step_id, input_json, attempt, sequence, output_json, replay_json, metadata_json, committed_at) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
358
|
+
.run(checkpoint.runId, checkpoint.sessionId, checkpoint.leaseId, checkpoint.workerId, checkpoint.stepId, inputJson, checkpoint.attempt, checkpoint.sequence, outputJson, replayJson, metadataJson, checkpoint.committedAt ?? this.nowIso());
|
|
359
|
+
});
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
async withSessionLock(sessionId, fn) {
|
|
363
|
+
let lock = this.sessionLocks.get(sessionId);
|
|
364
|
+
if (!lock) {
|
|
365
|
+
lock = new AsyncMutex();
|
|
366
|
+
this.sessionLocks.set(sessionId, lock);
|
|
367
|
+
}
|
|
368
|
+
return lock.lock(fn);
|
|
369
|
+
}
|
|
370
|
+
async write(checkpoint, opts = {}) {
|
|
371
|
+
return this.contextSpan('write', {
|
|
372
|
+
'harness.context_checkpoint.kind': checkpoint.kind,
|
|
373
|
+
'harness.context_checkpoint.sequence': checkpoint.sequence,
|
|
374
|
+
'harness.context_checkpoint.ref_hash': contextRefHash(checkpoint),
|
|
375
|
+
'harness.context_checkpoint.payload_size_bytes': checkpoint.payloadSizeBytes,
|
|
376
|
+
'harness.run.id': checkpoint.runId,
|
|
377
|
+
'harness.session.id': checkpoint.sessionId,
|
|
378
|
+
...(checkpoint.workflowId ? { 'harness.workflow.id': checkpoint.workflowId } : {}),
|
|
379
|
+
...(checkpoint.agentId ? { 'harness.agent.id': checkpoint.agentId } : {})
|
|
380
|
+
}, async () => {
|
|
381
|
+
throwIfAborted(opts.signal);
|
|
382
|
+
this.stmt('insert into harness_context_checkpoints(run_id, session_id, workflow_id, agent_id, sequence, kind, payload_json, payload_size_bytes, created_at, metadata_json) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?) on conflict(run_id, session_id, sequence, kind) do update set payload_json=excluded.payload_json, payload_size_bytes=excluded.payload_size_bytes, created_at=excluded.created_at, metadata_json=excluded.metadata_json')
|
|
383
|
+
.run(checkpoint.runId, checkpoint.sessionId, checkpoint.workflowId ?? null, checkpoint.agentId ?? null, checkpoint.sequence, checkpoint.kind, JSON.stringify(checkpoint.payload), checkpoint.payloadSizeBytes, checkpoint.createdAt, stringify(checkpoint.metadata));
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
async list(query) {
|
|
387
|
+
return this.contextSpan('list', {
|
|
388
|
+
'harness.context_checkpoint.limit': query.limit ?? 100,
|
|
389
|
+
...(query.kind ? { 'harness.context_checkpoint.kind': query.kind } : {}),
|
|
390
|
+
...(query.runId ? { 'harness.run.id': query.runId } : {}),
|
|
391
|
+
...(query.sessionId ? { 'harness.session.id': query.sessionId } : {}),
|
|
392
|
+
...(query.workflowId ? { 'harness.workflow.id': query.workflowId } : {}),
|
|
393
|
+
...(query.agentId ? { 'harness.agent.id': query.agentId } : {})
|
|
394
|
+
}, async (recordAttrs) => {
|
|
395
|
+
throwIfAborted(query.signal);
|
|
396
|
+
const clauses = [];
|
|
397
|
+
const params = [];
|
|
398
|
+
for (const [column, value] of [
|
|
399
|
+
['run_id', query.runId],
|
|
400
|
+
['session_id', query.sessionId],
|
|
401
|
+
['workflow_id', query.workflowId],
|
|
402
|
+
['agent_id', query.agentId],
|
|
403
|
+
['kind', query.kind]
|
|
404
|
+
]) {
|
|
405
|
+
if (value !== undefined) {
|
|
406
|
+
clauses.push(`${column} = ?`);
|
|
407
|
+
params.push(value);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const where = clauses.length > 0 ? `where ${clauses.join(' and ')}` : '';
|
|
411
|
+
const limit = query.limit ?? 100;
|
|
412
|
+
const rows = this.stmt(`select * from harness_context_checkpoints ${where} order by sequence asc limit ?`).all(...params, limit);
|
|
413
|
+
recordAttrs({ 'harness.context_checkpoint.result_count': rows.length });
|
|
414
|
+
return rows.map((row) => this.rowToContextCheckpoint(row));
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
async read(ref) {
|
|
418
|
+
return this.contextSpan('read', {
|
|
419
|
+
'harness.context_checkpoint.kind': ref.kind,
|
|
420
|
+
'harness.context_checkpoint.sequence': ref.sequence,
|
|
421
|
+
'harness.context_checkpoint.ref_hash': contextRefHash(ref),
|
|
422
|
+
'harness.run.id': ref.runId,
|
|
423
|
+
'harness.session.id': ref.sessionId
|
|
424
|
+
}, async () => {
|
|
425
|
+
const row = this.stmt('select * from harness_context_checkpoints where run_id = ? and session_id = ? and sequence = ? and kind = ?')
|
|
426
|
+
.get(ref.runId, ref.sessionId, ref.sequence, ref.kind);
|
|
427
|
+
return row ? this.rowToContextCheckpoint(row) : undefined;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async delete(ref) {
|
|
431
|
+
return this.contextSpan('delete', {
|
|
432
|
+
'harness.context_checkpoint.kind': ref.kind,
|
|
433
|
+
'harness.context_checkpoint.sequence': ref.sequence,
|
|
434
|
+
'harness.context_checkpoint.ref_hash': contextRefHash(ref),
|
|
435
|
+
'harness.run.id': ref.runId,
|
|
436
|
+
'harness.session.id': ref.sessionId
|
|
437
|
+
}, async () => {
|
|
438
|
+
this.stmt('delete from harness_context_checkpoints where run_id = ? and session_id = ? and sequence = ? and kind = ?')
|
|
439
|
+
.run(ref.runId, ref.sessionId, ref.sequence, ref.kind);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async close() {
|
|
443
|
+
if (this.closed)
|
|
444
|
+
return;
|
|
445
|
+
this.closed = true;
|
|
446
|
+
this.statements.clear();
|
|
447
|
+
this.db.close();
|
|
448
|
+
}
|
|
449
|
+
migrate() {
|
|
450
|
+
this.db.exec(`
|
|
451
|
+
pragma journal_mode = WAL;
|
|
452
|
+
pragma foreign_keys = ON;
|
|
453
|
+
pragma busy_timeout = 5000;
|
|
454
|
+
create table if not exists harness_sessions(id text primary key, created_at text not null, updated_at text not null, run_count integer not null, metadata_json text);
|
|
455
|
+
create table if not exists harness_messages(id text primary key, session_id text not null, role text not null, content text not null, tool_calls_json text, tool_results_json text, timestamp text not null);
|
|
456
|
+
create index if not exists idx_harness_messages_session_order on harness_messages(session_id, timestamp, id);
|
|
457
|
+
create table if not exists harness_runs(id text primary key, session_id text not null, kind text not null, target text not null, started_at text not null, finished_at text, status text not null, input_json text, output_json text, error_json text);
|
|
458
|
+
create index if not exists idx_harness_runs_session_order on harness_runs(session_id, started_at, id);
|
|
459
|
+
create table if not exists harness_run_events(id text primary key, run_id text not null, at text not null, type text not null, payload_json text not null);
|
|
460
|
+
create index if not exists idx_harness_run_events_run_order on harness_run_events(run_id, id);
|
|
461
|
+
create table if not exists harness_durable_runs(run_id text primary key, session_id text not null, worker_id text not null, step_id text not null, input_json text not null, attempt integer not null, status text not null, metadata_json text, output_json text, error_json text, started_at text not null, finished_at text);
|
|
462
|
+
create table if not exists harness_durable_checkpoints(run_id text not null, session_id text not null, lease_id text not null, worker_id text not null, step_id text not null, input_json text not null, attempt integer not null, sequence integer not null, output_json text, replay_json text, metadata_json text, committed_at text not null, primary key(run_id, step_id));
|
|
463
|
+
create index if not exists idx_harness_durable_checkpoints_order on harness_durable_checkpoints(run_id, sequence);
|
|
464
|
+
create table if not exists harness_durable_leases(run_id text primary key, session_id text not null, worker_id text not null, lease_id text not null, expires_at text not null);
|
|
465
|
+
create index if not exists idx_harness_durable_leases_session on harness_durable_leases(session_id);
|
|
466
|
+
create table if not exists harness_context_checkpoints(run_id text not null, session_id text not null, workflow_id text, agent_id text, sequence integer not null, kind text not null, payload_json text not null, payload_size_bytes integer not null, created_at text not null, metadata_json text, primary key(run_id, session_id, sequence, kind));
|
|
467
|
+
`);
|
|
468
|
+
}
|
|
469
|
+
stmt(sql) {
|
|
470
|
+
let statement = this.statements.get(sql);
|
|
471
|
+
if (!statement) {
|
|
472
|
+
statement = this.db.prepare(sql);
|
|
473
|
+
this.statements.set(sql, statement);
|
|
474
|
+
}
|
|
475
|
+
return statement;
|
|
476
|
+
}
|
|
477
|
+
nowIso() {
|
|
478
|
+
return new Date(this.clock()).toISOString();
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Runs a synchronous statement batch inside a single SQLite transaction.
|
|
482
|
+
* The in-process mutex guarantees only one open transaction per connection;
|
|
483
|
+
* the callback must stay synchronous so the transaction never spans an await.
|
|
484
|
+
*/
|
|
485
|
+
async transaction(fn) {
|
|
486
|
+
return this.dbLock.lock(async () => {
|
|
487
|
+
this.db.exec('begin immediate');
|
|
488
|
+
try {
|
|
489
|
+
const result = fn();
|
|
490
|
+
this.db.exec('commit');
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
this.db.exec('rollback');
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
loadRun(runId) {
|
|
500
|
+
const row = this.stmt('select * from harness_runs where id = ?').get(runId);
|
|
501
|
+
return row ? this.rowToRun(row) : undefined;
|
|
502
|
+
}
|
|
503
|
+
loadDurableRun(runId) {
|
|
504
|
+
const row = this.stmt('select status, attempt from harness_durable_runs where run_id = ?').get(runId);
|
|
505
|
+
return row ? { status: requiredString(row, 'status', 'getRun'), attempt: requiredNumber(row, 'attempt', 'getRun') } : undefined;
|
|
506
|
+
}
|
|
507
|
+
assertLeaseAvailable(runId, sessionId, workerId) {
|
|
508
|
+
const nowIso = this.nowIso();
|
|
509
|
+
// Scoped expiry: only clear stale leases for the contested run/session so
|
|
510
|
+
// an unrelated long-running lease is never deleted by another start.
|
|
511
|
+
this.stmt('delete from harness_durable_leases where run_id = ? and expires_at < ?').run(runId, nowIso);
|
|
512
|
+
this.stmt('delete from harness_durable_leases where session_id = ? and expires_at < ?').run(sessionId, nowIso);
|
|
513
|
+
const runLease = this.stmt('select * from harness_durable_leases where run_id = ?').get(runId);
|
|
514
|
+
if (runLease && runLease['worker_id'] !== workerId)
|
|
515
|
+
throw new DurableRunLeaseError(`Durable run "${runId}" is already owned by worker "${runLease['worker_id']}".`);
|
|
516
|
+
const sessionLease = this.stmt('select * from harness_durable_leases where session_id = ? and run_id != ?').get(sessionId, runId);
|
|
517
|
+
if (sessionLease && sessionLease['worker_id'] !== workerId)
|
|
518
|
+
throw new DurableRunLeaseError(`Durable session "${sessionId}" is already owned by another worker.`);
|
|
519
|
+
}
|
|
520
|
+
toLease(runId, leaseId) {
|
|
521
|
+
const run = this.stmt('select * from harness_durable_runs where run_id = ?').get(runId);
|
|
522
|
+
if (!run)
|
|
523
|
+
throw new DurableRunLeaseError(`Durable run "${runId}" has not been started.`);
|
|
524
|
+
const checkpoints = this.stmt('select * from harness_durable_checkpoints where run_id = ? order by sequence asc').all(runId).map((row) => this.rowToCheckpoint(row));
|
|
525
|
+
const latest = checkpoints.at(-1);
|
|
526
|
+
return {
|
|
527
|
+
runId,
|
|
528
|
+
sessionId: requiredString(run, 'session_id', 'getRun'),
|
|
529
|
+
workerId: requiredString(run, 'worker_id', 'getRun'),
|
|
530
|
+
leaseId,
|
|
531
|
+
attempt: requiredNumber(run, 'attempt', 'getRun'),
|
|
532
|
+
resumed: checkpoints.length > 0,
|
|
533
|
+
start: {
|
|
534
|
+
runId,
|
|
535
|
+
sessionId: requiredString(run, 'session_id', 'getRun'),
|
|
536
|
+
workerId: requiredString(run, 'worker_id', 'getRun'),
|
|
537
|
+
stepId: requiredString(run, 'step_id', 'getRun'),
|
|
538
|
+
input: parseJson(run['input_json']) ?? null,
|
|
539
|
+
attempt: requiredNumber(run, 'attempt', 'getRun'),
|
|
540
|
+
...optional('metadata', parseJson(run['metadata_json']))
|
|
541
|
+
},
|
|
542
|
+
...(latest ? { checkpoint: latest } : {}),
|
|
543
|
+
checkpoints,
|
|
544
|
+
release: async () => {
|
|
545
|
+
this.stmt('delete from harness_durable_leases where run_id = ? and lease_id = ?').run(runId, leaseId);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
rowToSession(row) {
|
|
550
|
+
return {
|
|
551
|
+
id: requiredString(row, 'id', 'getSession'),
|
|
552
|
+
createdAt: requiredString(row, 'created_at', 'getSession'),
|
|
553
|
+
updatedAt: requiredString(row, 'updated_at', 'getSession'),
|
|
554
|
+
runCount: requiredNumber(row, 'run_count', 'getSession'),
|
|
555
|
+
...optional('metadata', parseJson(row['metadata_json']))
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
rowToMessage(row) {
|
|
559
|
+
const toolCalls = parseJson(row['tool_calls_json']);
|
|
560
|
+
const toolResults = parseJson(row['tool_results_json']);
|
|
561
|
+
return {
|
|
562
|
+
id: requiredString(row, 'id', 'listMessages'),
|
|
563
|
+
sessionId: requiredString(row, 'session_id', 'listMessages'),
|
|
564
|
+
role: requiredString(row, 'role', 'listMessages'),
|
|
565
|
+
content: requiredString(row, 'content', 'listMessages'),
|
|
566
|
+
...optional('toolCalls', toolCalls),
|
|
567
|
+
...optional('toolResults', toolResults),
|
|
568
|
+
timestamp: requiredString(row, 'timestamp', 'listMessages')
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
rowToRun(row) {
|
|
572
|
+
const input = parseJson(row['input_json']);
|
|
573
|
+
const output = parseJson(row['output_json']);
|
|
574
|
+
const error = parseJson(row['error_json']);
|
|
575
|
+
return {
|
|
576
|
+
id: requiredString(row, 'id', 'getRun'),
|
|
577
|
+
sessionId: requiredString(row, 'session_id', 'getRun'),
|
|
578
|
+
kind: requiredString(row, 'kind', 'getRun'),
|
|
579
|
+
target: requiredString(row, 'target', 'getRun'),
|
|
580
|
+
startedAt: requiredString(row, 'started_at', 'getRun'),
|
|
581
|
+
...(row['finished_at'] ? { finishedAt: requiredString(row, 'finished_at', 'getRun') } : {}),
|
|
582
|
+
status: requiredString(row, 'status', 'getRun'),
|
|
583
|
+
...optional('input', input),
|
|
584
|
+
...optional('output', output),
|
|
585
|
+
...optional('error', error)
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
rowToCheckpoint(row) {
|
|
589
|
+
const output = parseJson(row['output_json']);
|
|
590
|
+
const replay = parseJson(row['replay_json']);
|
|
591
|
+
const metadata = parseJson(row['metadata_json']);
|
|
592
|
+
return {
|
|
593
|
+
runId: requiredString(row, 'run_id', 'getRun'),
|
|
594
|
+
sessionId: requiredString(row, 'session_id', 'getRun'),
|
|
595
|
+
leaseId: requiredString(row, 'lease_id', 'getRun'),
|
|
596
|
+
workerId: requiredString(row, 'worker_id', 'getRun'),
|
|
597
|
+
stepId: requiredString(row, 'step_id', 'getRun'),
|
|
598
|
+
input: parseJson(row['input_json']) ?? null,
|
|
599
|
+
attempt: requiredNumber(row, 'attempt', 'getRun'),
|
|
600
|
+
sequence: requiredNumber(row, 'sequence', 'getRun'),
|
|
601
|
+
...optional('output', output),
|
|
602
|
+
...optional('replay', replay),
|
|
603
|
+
...optional('metadata', metadata),
|
|
604
|
+
committedAt: requiredString(row, 'committed_at', 'getRun')
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
rowToContextCheckpoint(row) {
|
|
608
|
+
const metadata = parseJson(row['metadata_json']);
|
|
609
|
+
return {
|
|
610
|
+
runId: requiredString(row, 'run_id', 'contextCheckpointRead'),
|
|
611
|
+
sessionId: requiredString(row, 'session_id', 'contextCheckpointRead'),
|
|
612
|
+
...(row['workflow_id'] ? { workflowId: requiredString(row, 'workflow_id', 'contextCheckpointRead') } : {}),
|
|
613
|
+
...(row['agent_id'] ? { agentId: requiredString(row, 'agent_id', 'contextCheckpointRead') } : {}),
|
|
614
|
+
sequence: requiredNumber(row, 'sequence', 'contextCheckpointRead'),
|
|
615
|
+
kind: requiredString(row, 'kind', 'contextCheckpointRead'),
|
|
616
|
+
payload: parseJson(row['payload_json']) ?? null,
|
|
617
|
+
payloadSizeBytes: requiredNumber(row, 'payload_size_bytes', 'contextCheckpointRead'),
|
|
618
|
+
createdAt: requiredString(row, 'created_at', 'contextCheckpointRead'),
|
|
619
|
+
...optional('metadata', metadata)
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
async runtimeSpan(operation, attrs, fn) {
|
|
623
|
+
return this.operationSpan('harness.runtime', 'harness.runtime.operation.duration', 'harness.runtime.operations', {
|
|
624
|
+
'harness.runtime.adapter': this.id,
|
|
625
|
+
'harness.runtime.operation': operation,
|
|
626
|
+
'harness.runtime.persistent': true,
|
|
627
|
+
...attrs
|
|
628
|
+
}, fn);
|
|
629
|
+
}
|
|
630
|
+
async contextSpan(operation, attrs, fn) {
|
|
631
|
+
return this.operationSpan('harness.context_checkpoint', 'harness.context_checkpoint.operation.duration', 'harness.context_checkpoint.operations', {
|
|
632
|
+
'harness.context_checkpoint.adapter': this.info.id,
|
|
633
|
+
'harness.context_checkpoint.operation': operation,
|
|
634
|
+
...attrs
|
|
635
|
+
}, fn);
|
|
636
|
+
}
|
|
637
|
+
async operationSpan(prefix, histogram, counter, attrs, fn) {
|
|
638
|
+
const merged = { ...attrs };
|
|
639
|
+
const started = Date.now();
|
|
640
|
+
const run = async (span) => {
|
|
641
|
+
const recordAttrs = (extra) => {
|
|
642
|
+
Object.assign(merged, extra);
|
|
643
|
+
span?.setAttributes(definedAttrs(extra));
|
|
644
|
+
};
|
|
645
|
+
try {
|
|
646
|
+
const result = await fn(recordAttrs);
|
|
647
|
+
this.telemetry?.recordCounter(counter, 1, merged);
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
this.telemetry?.recordHistogram(histogram, (Date.now() - started) / 1000, merged);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
return this.telemetry ? this.telemetry.span(`${prefix}.${String(merged[`${prefix}.operation`] ?? 'operation')}`, merged, (span) => run(span)) : run();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function definedAttrs(attrs) {
|
|
658
|
+
const out = {};
|
|
659
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
660
|
+
if (value !== undefined)
|
|
661
|
+
out[key] = value;
|
|
662
|
+
}
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
function optional(key, value) {
|
|
666
|
+
return (value === undefined ? {} : { [key]: value });
|
|
667
|
+
}
|
|
668
|
+
function throwIfAborted(signal) {
|
|
669
|
+
if (signal?.aborted)
|
|
670
|
+
throw new OperationCancelledError('Context checkpoint operation was cancelled.', { scope: 'workspace' });
|
|
671
|
+
}
|
|
672
|
+
export function sqliteDurableRuntime(options) {
|
|
673
|
+
return new SqliteHarnessStorage(options);
|
|
674
|
+
}
|
|
675
|
+
export function sqliteStateStore(options) {
|
|
676
|
+
return new SqliteHarnessStorage(options);
|
|
677
|
+
}
|
|
678
|
+
export function sqliteContextCheckpointStore(options) {
|
|
679
|
+
return new SqliteHarnessStorage(options);
|
|
680
|
+
}
|