trajectories 0.1.2 → 0.1.4

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.
@@ -0,0 +1,286 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { TrajectoriesError } from './log-types.js';
4
+ import { validateTranscript } from '@letta-ai/trajectory';
5
+ import { digest, getObject, safeId, stableJson, withSessionLock } from './log-io.js';
6
+ export function sharedObservationId(machineId, sessionId) {
7
+ safeId(machineId);
8
+ if (typeof sessionId !== 'string' || !sessionId || sessionId.length > 4096)
9
+ throw new TrajectoriesError('INVALID_INPUT', 'An indexed session identity is required.');
10
+ return digest(stableJson([machineId, sessionId]));
11
+ }
12
+ const hash = (value) => {
13
+ if (typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value))
14
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid shared content identity.');
15
+ return value;
16
+ };
17
+ const observationPath = (store, id) => join(resolve(store), 'observations', `${hash(id)}.jsonl`);
18
+ function assertSession(value) {
19
+ if (!value || typeof value !== 'object' || typeof value.id !== 'string' || !value.id
20
+ || typeof value.source !== 'string' || !value.source || typeof value.source_path !== 'string'
21
+ || typeof value.content_hash !== 'string' || !Number.isSafeInteger(value.record_count) || value.record_count < 0
22
+ || typeof value.created_at !== 'string' || typeof value.updated_at !== 'string')
23
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid canonical observed session metadata.');
24
+ for (const key of ['title', 'cwd', 'model', 'started_at', 'ended_at'])
25
+ if (value[key] !== null && typeof value[key] !== 'string')
26
+ throw new TrajectoriesError('INVALID_INPUT', `Invalid session ${key}.`);
27
+ for (const key of ['parent_session_id', 'parent_tool_use_id', 'agent_nickname', 'thread_role'])
28
+ if (value[key] !== undefined && value[key] !== null && typeof value[key] !== 'string')
29
+ throw new TrajectoriesError('INVALID_INPUT', `Invalid session ${key}.`);
30
+ if (value.content_hash_version !== undefined && ![1, 2].includes(value.content_hash_version))
31
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid observed hash version.');
32
+ if (value.diagnostics !== undefined) {
33
+ if (!Array.isArray(value.diagnostics))
34
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid observed diagnostics.');
35
+ const codes = new Set();
36
+ for (const item of value.diagnostics) {
37
+ if (!item || typeof item.code !== 'string' || !item.code || codes.has(item.code)
38
+ || !Number.isSafeInteger(item.occurrenceCount) || item.occurrenceCount < 0
39
+ || (item.sample !== null && typeof item.sample !== 'string')
40
+ || (item.firstSeenAt !== undefined && typeof item.firstSeenAt !== 'string'))
41
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid observed diagnostic aggregate.');
42
+ codes.add(item.code);
43
+ }
44
+ }
45
+ }
46
+ function assertRecord(value, sessionId) {
47
+ if (!value || typeof value !== 'object' || typeof value.id !== 'string' || !value.id
48
+ || value.session_id !== sessionId || typeof value.source_type !== 'string' || typeof value.record_type !== 'string'
49
+ || typeof value.source_order_id !== 'string' || !Number.isSafeInteger(value.component_index) || value.component_index < 0
50
+ || typeof value.record_hash !== 'string' || typeof value.content_hash !== 'string' || typeof value.record_json !== 'string')
51
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid canonical observed record or session identity.');
52
+ for (const key of ['content', 'tool_call_id', 'tool_name', 'tool_arguments_json', 'tool_result_json',
53
+ 'record_timestamp', 'source_timestamp', 'stable_source_record_id', 'source_identity_kind'])
54
+ if (value[key] !== null && typeof value[key] !== 'string')
55
+ throw new TrajectoriesError('INVALID_INPUT', `Invalid record ${key}.`);
56
+ if (![null, 0, 1].includes(value.tool_result_ok))
57
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid tool outcome.');
58
+ if (value.source_identity_kind !== null && !['native', 'location', 'content', 'synthetic'].includes(value.source_identity_kind))
59
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid source identity kind.');
60
+ for (const key of ['native_session_id', 'binding_id'])
61
+ if (value[key] !== undefined && value[key] !== null && typeof value[key] !== 'string')
62
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid native lineage identity.');
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(value.record_json);
66
+ }
67
+ catch {
68
+ throw new TrajectoriesError('INVALID_INPUT', 'Malformed canonical record JSON.');
69
+ }
70
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
71
+ throw new TrajectoriesError('INVALID_INPUT', 'Canonical record JSON must be an object.');
72
+ try {
73
+ validateTranscript([parsed], { partial: true });
74
+ }
75
+ catch {
76
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid normalized canonical record.');
77
+ }
78
+ const record = parsed;
79
+ if (value.record_type !== (record.role === 'assistant' && record.tool_calls ? 'assistant-tool-call' : record.role)
80
+ || digest(value.record_json) !== value.record_hash)
81
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Canonical record type or digest disagrees with its payload.');
82
+ }
83
+ export function assertObservedSummary(value, id) {
84
+ if (!value || typeof value !== 'object')
85
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid shared observation.');
86
+ assertSession(value.session);
87
+ hash(value.head);
88
+ if ((id !== undefined && value.id !== id) || value.id !== sharedObservationId(value.machineId, value.session.id))
89
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Shared observation identity mismatch.');
90
+ }
91
+ export function assertObservedManifest(value, id) {
92
+ assertObservedSummary(value, id);
93
+ if (!Array.isArray(value.recordObjects))
94
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid shared record manifest.');
95
+ const ids = new Set();
96
+ for (const ref of value.recordObjects) {
97
+ if (!ref || typeof ref.id !== 'string' || !ref.id || ids.has(ref.id))
98
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid shared record identities.');
99
+ ids.add(ref.id);
100
+ hash(ref.object);
101
+ }
102
+ if (ids.size !== value.session.record_count)
103
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Shared manifest count mismatch.');
104
+ }
105
+ export function assertObservedState(value, id) {
106
+ assertObservedSummary(value, id);
107
+ if (!Array.isArray(value.records))
108
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid shared canonical records.');
109
+ const ids = new Set();
110
+ for (const record of value.records) {
111
+ assertRecord(record, value.session.id);
112
+ if (ids.has(record.id))
113
+ throw new TrajectoriesError('INVALID_INPUT', 'Duplicate shared canonical record.');
114
+ ids.add(record.id);
115
+ }
116
+ if (ids.size !== value.session.record_count)
117
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Shared record count mismatch.');
118
+ }
119
+ async function frames(store, id) {
120
+ let text;
121
+ try {
122
+ text = await readFile(observationPath(store, id), 'utf8');
123
+ }
124
+ catch (error) {
125
+ if (error.code === 'ENOENT')
126
+ return [];
127
+ throw error;
128
+ }
129
+ if (!text.endsWith('\n'))
130
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Partial observed-history log.');
131
+ const output = [];
132
+ for (const line of text.trimEnd().split('\n')) {
133
+ let frame;
134
+ try {
135
+ frame = JSON.parse(line);
136
+ }
137
+ catch {
138
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Invalid observed-history JSON.');
139
+ }
140
+ const body = frame?.body;
141
+ if (!body || body.version !== 1 || body.id !== id || sharedObservationId(body.machineId, body.sessionId) !== id
142
+ || body.parent !== (output.at(-1)?.head ?? null) || digest(stableJson(body)) !== frame.head
143
+ || !Array.isArray(body.set) || !Array.isArray(body.remove))
144
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Invalid observed-history chain.');
145
+ output.push(frame);
146
+ }
147
+ return output;
148
+ }
149
+ async function materialize(store, id, history) {
150
+ const current = history.at(-1);
151
+ if (!current)
152
+ throw new TrajectoriesError('NOT_FOUND', 'Shared observed session not found.');
153
+ const refs = new Map();
154
+ for (const { body } of history) {
155
+ for (const key of body.remove)
156
+ refs.delete(key);
157
+ for (const item of body.set)
158
+ refs.set(item.id, hash(item.object));
159
+ }
160
+ const session = JSON.parse((await getObject(store, hash(current.body.sessionObject))).toString());
161
+ assertSession(session);
162
+ if (session.id !== current.body.sessionId || session.record_count !== refs.size)
163
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Observed session metadata disagrees with its record view.');
164
+ return { id, head: current.head, machineId: current.body.machineId, session,
165
+ recordObjects: [...refs].map(([recordId, object]) => ({ id: recordId, object })) };
166
+ }
167
+ export async function getObservedManifest(store, id) {
168
+ return materialize(store, id, await frames(store, id));
169
+ }
170
+ export async function readObservedHistory(store, id) {
171
+ const { recordObjects, ...summary } = await getObservedManifest(store, id);
172
+ const records = [];
173
+ for (const ref of recordObjects) {
174
+ const record = JSON.parse((await getObject(store, ref.object)).toString());
175
+ assertRecord(record, summary.session.id);
176
+ if (record.id !== ref.id)
177
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Observed record identity changed.');
178
+ records.push(record);
179
+ }
180
+ records.sort((a, b) => a.source_order_id < b.source_order_id ? -1 : a.source_order_id > b.source_order_id ? 1
181
+ : a.component_index - b.component_index || a.id.localeCompare(b.id));
182
+ return { ...summary, records };
183
+ }
184
+ export async function listObservedHistories(store) {
185
+ let names;
186
+ try {
187
+ names = await readdir(join(resolve(store), 'observations'));
188
+ }
189
+ catch (error) {
190
+ if (error.code === 'ENOENT')
191
+ return [];
192
+ throw error;
193
+ }
194
+ const output = [];
195
+ for (const name of names.sort()) {
196
+ if (!/^[a-f0-9]{64}\.jsonl$/.test(name))
197
+ continue;
198
+ const { recordObjects, ...summary } = await getObservedManifest(store, name.slice(0, -6));
199
+ output.push(summary);
200
+ }
201
+ return output;
202
+ }
203
+ export async function commitObservedDelta(store, delta) {
204
+ if (!delta || delta.version !== 1 || delta.id !== sharedObservationId(delta.machineId, delta.sessionId)
205
+ || (delta.parent !== null && hash(delta.parent) !== delta.parent) || !Array.isArray(delta.set) || !Array.isArray(delta.remove))
206
+ throw new TrajectoriesError('INVALID_INPUT', 'Invalid observed-history delta.');
207
+ hash(delta.sessionObject);
208
+ const keys = new Set();
209
+ for (const item of delta.set) {
210
+ if (!item || typeof item.id !== 'string' || !item.id || keys.has(item.id))
211
+ throw new TrajectoriesError('INVALID_INPUT', 'Duplicate or invalid record identity.');
212
+ keys.add(item.id);
213
+ hash(item.object);
214
+ }
215
+ for (const key of delta.remove) {
216
+ if (typeof key !== 'string' || !key || keys.has(key))
217
+ throw new TrajectoriesError('INVALID_INPUT', 'Duplicate or contradictory record changes.');
218
+ keys.add(key);
219
+ }
220
+ return withSessionLock(store, delta.id, async () => {
221
+ const history = await frames(store, delta.id);
222
+ const head = digest(stableJson(delta));
223
+ if (history.at(-1)?.head === head) {
224
+ const { recordObjects, ...summary } = await materialize(store, delta.id, history);
225
+ return summary;
226
+ }
227
+ if ((history.at(-1)?.head ?? null) !== delta.parent)
228
+ throw new TrajectoriesError('STALE_HEAD', 'Shared observed history advanced; refresh before publishing.');
229
+ for (const item of delta.set) {
230
+ const record = JSON.parse((await getObject(store, item.object)).toString());
231
+ assertRecord(record, delta.sessionId);
232
+ if (record.id !== item.id)
233
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Observed record reference has a different identity.');
234
+ }
235
+ const prospective = [...history, { head, body: delta }];
236
+ const manifest = await materialize(store, delta.id, prospective);
237
+ // This publishes delta events only, not a whole observation snapshot.
238
+ const { open } = await import('node:fs/promises');
239
+ const { ensureDirectory, syncDirectory } = await import('./log-io.js');
240
+ const path = observationPath(store, delta.id);
241
+ await ensureDirectory(join(resolve(store), 'observations'));
242
+ const file = await open(path, 'a', 0o600);
243
+ try {
244
+ await file.writeFile(stableJson({ head, body: delta }) + '\n');
245
+ await file.sync();
246
+ }
247
+ finally {
248
+ await file.close();
249
+ }
250
+ await syncDirectory(join(resolve(store), 'observations'));
251
+ const { recordObjects, ...summary } = manifest;
252
+ return summary;
253
+ });
254
+ }
255
+ export async function prepareObservedHistory(input, previous) {
256
+ assertSession(input.session);
257
+ const id = sharedObservationId(input.machineId, input.session.id);
258
+ if (previous)
259
+ assertObservedManifest(previous, id);
260
+ if (previous && (previous.id !== id || previous.machineId !== input.machineId || previous.session.id !== input.session.id))
261
+ throw new TrajectoriesError('INTEGRITY_ERROR', 'Remote observation identity mismatch.');
262
+ const prior = new Map(previous?.recordObjects.map(item => [item.id, item.object]) ?? []);
263
+ const objects = new Map();
264
+ const add = (value) => { const bytes = Buffer.from(stableJson(value)); const key = digest(bytes); objects.set(key, bytes); return key; };
265
+ const sessionObject = add(input.session);
266
+ const seen = new Set();
267
+ const set = [];
268
+ for (const record of input.records) {
269
+ assertRecord(record, input.session.id);
270
+ if (seen.has(record.id))
271
+ throw new TrajectoriesError('INVALID_INPUT', 'Duplicate canonical record ID.');
272
+ seen.add(record.id);
273
+ const key = digest(stableJson(record));
274
+ if (prior.get(record.id) !== key) {
275
+ add(record);
276
+ set.push({ id: record.id, object: key });
277
+ }
278
+ }
279
+ if (input.session.record_count !== seen.size)
280
+ throw new TrajectoriesError('INVALID_INPUT', 'Session record count disagrees with canonical records.');
281
+ const remove = [...prior.keys()].filter(key => !seen.has(key));
282
+ if (previous && !set.length && !remove.length && stableJson(previous.session) === stableJson(input.session))
283
+ return { delta: null, objects: new Map(), changedRecords: 0, removedRecords: 0 };
284
+ return { delta: { version: 1, id, parent: previous?.head ?? null, machineId: input.machineId,
285
+ sessionId: input.session.id, sessionObject, set, remove }, objects, changedRecords: set.length, removedRecords: remove.length };
286
+ }
@@ -0,0 +1,40 @@
1
+ import { type ManagedSession } from './log-types.js';
2
+ import { type ObservedRecord, type ObservedSession, type ObservedState, type ObservedSummary } from './shared-observed.js';
3
+ export { sharedObservationId } from './shared-observed.js';
4
+ export type { ObservedRecord, ObservedSession, ObservedState, ObservedSummary } from './shared-observed.js';
5
+ export type { ManagedDelta } from './shared-managed.js';
6
+ export interface SharedTransfer {
7
+ id: string;
8
+ head: string;
9
+ objectsTransferred: number;
10
+ bytesTransferred: number;
11
+ }
12
+ export interface ObservedSyncReceipt extends SharedTransfer {
13
+ changedRecords: number;
14
+ removedRecords: number;
15
+ }
16
+ export interface ManagedSyncReceipt extends SharedTransfer {
17
+ framesTransferred: number;
18
+ }
19
+ /** Node-compatible fetch handler; the host owns sockets, TLS, credentials and process lifetime. */
20
+ export declare function createSharedHandler(options: {
21
+ store: string;
22
+ token: string;
23
+ maxBytes?: number;
24
+ }): (request: Request) => Promise<Response>;
25
+ export declare function createSharedClient(options: {
26
+ url: string;
27
+ token: string;
28
+ signal?: AbortSignal;
29
+ }): {
30
+ listObserved(): Promise<ObservedSummary[]>;
31
+ getObserved(id: string): Promise<ObservedState>;
32
+ listManaged(): Promise<ManagedSession[]>;
33
+ pushObserved(input: {
34
+ machineId: string;
35
+ session: ObservedSession;
36
+ records: ObservedRecord[];
37
+ }): Promise<ObservedSyncReceipt>;
38
+ pushManaged(store: string, id: string): Promise<ManagedSyncReceipt>;
39
+ pullManaged(store: string, id: string): Promise<ManagedSyncReceipt>;
40
+ };