@modelprofile.com/flexharness 10.23.0 → 10.24.1
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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/dist_ts_compaction/plugins.d.ts +2 -2
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +9 -0
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +13 -1
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +2 -2
- package/dist_ts_stores_nosqldb/classes.recordstore.js +3 -3
- package/package.json +3 -3
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/readme.md +1 -1
- package/ts/utils.flexharness.ts +121 -0
- package/ts_compaction/plugins.ts +2 -2
- package/ts_compaction/smartagent.compaction.ts +5 -4
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +13 -0
- package/ts_stores_nosqldb/classes.recordstore.ts +5 -5
- package/ts_stores_nosqldb/readme.md +7 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessSessionBusyError,
|
|
5
|
+
FlexHarnessValidationError,
|
|
6
|
+
} from './errors.js';
|
|
7
|
+
import {
|
|
8
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS,
|
|
9
|
+
type IFlexCreateSessionOptions,
|
|
10
|
+
type IFlexEventArchiveMetadata,
|
|
11
|
+
type IFlexMessage,
|
|
12
|
+
type IFlexMessagePage,
|
|
13
|
+
type IFlexMessagePageOptions,
|
|
14
|
+
type IFlexSession,
|
|
15
|
+
type IFlexSessionView,
|
|
16
|
+
type IFlexSessionMetrics,
|
|
17
|
+
type IFlexSessionTombstone,
|
|
18
|
+
type IFlexUpdateSessionOptions,
|
|
19
|
+
type IJsonObject,
|
|
20
|
+
} from './interfaces.js';
|
|
21
|
+
import { cloneSerializable } from './utils.json.js';
|
|
22
|
+
import {
|
|
23
|
+
createBoundedTransferMessage,
|
|
24
|
+
jsonBytes,
|
|
25
|
+
maxMessagePageCursorBytes,
|
|
26
|
+
maxMessagePageSize,
|
|
27
|
+
maxTransferPageBytes,
|
|
28
|
+
publicSnapshot,
|
|
29
|
+
requireTransferIdentifier,
|
|
30
|
+
} from './utils.transfer.js';
|
|
31
|
+
import {
|
|
32
|
+
validateIdentifier,
|
|
33
|
+
validateSessionGenerationId,
|
|
34
|
+
validateSessionConfigurationRef,
|
|
35
|
+
validateUpdateSessionOptions,
|
|
36
|
+
validateUtf8String,
|
|
37
|
+
} from './utils.validation.js';
|
|
38
|
+
import { sessionMetricsView } from './utils.sessionmetrics.js';
|
|
39
|
+
import {
|
|
40
|
+
createSessionGenerationId,
|
|
41
|
+
requireSessionGeneration,
|
|
42
|
+
combineErrors,
|
|
43
|
+
} from './utils.flexharness.js';
|
|
44
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Consecutive waits a session view makes for queued writes and open sections to settle. Each wait
|
|
48
|
+
* ends only when real work finishes, so the bound limits starvation by a session that never stops
|
|
49
|
+
* changing, not the time one slow save may take.
|
|
50
|
+
*/
|
|
51
|
+
const maxSessionViewPasses = 64;
|
|
52
|
+
|
|
53
|
+
/** Sessions and their messages: listing, creation, updates, views and message pages. */
|
|
54
|
+
export class FlexHarnessSessions<TScope> {
|
|
55
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
56
|
+
|
|
57
|
+
public async listSessions(scopeId: string): Promise<IFlexSession[]> {
|
|
58
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
59
|
+
await state.scopeQueue;
|
|
60
|
+
this.core.assertOpen();
|
|
61
|
+
this.core.assertStateAcceptingWork(state);
|
|
62
|
+
return publicSnapshot([...state.sessions.values()]
|
|
63
|
+
.filter((stored) => !state.initializingSessions.has(stored.session.sessionId))
|
|
64
|
+
.map((stored) => stored.session)
|
|
65
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public async createSession(
|
|
69
|
+
scopeId: string,
|
|
70
|
+
options: IFlexCreateSessionOptions = {},
|
|
71
|
+
): Promise<IFlexSession> {
|
|
72
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
73
|
+
throw new FlexHarnessValidationError('createSession options must be a plain object.');
|
|
74
|
+
}
|
|
75
|
+
const unsupported = Object.keys(options).find(
|
|
76
|
+
(key) => !['sessionId', 'sessionGenerationId', 'configurationRef', 'title'].includes(key),
|
|
77
|
+
);
|
|
78
|
+
if (unsupported) throw new FlexHarnessValidationError(`createSession does not support "${unsupported}".`);
|
|
79
|
+
const requestedSessionGenerationId = options.sessionGenerationId;
|
|
80
|
+
if (requestedSessionGenerationId !== undefined) {
|
|
81
|
+
validateSessionGenerationId(requestedSessionGenerationId);
|
|
82
|
+
}
|
|
83
|
+
if (options.configurationRef !== undefined) validateSessionConfigurationRef(options.configurationRef);
|
|
84
|
+
const resolved = await this.core.resolveState(scopeId);
|
|
85
|
+
const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
|
|
86
|
+
validateIdentifier(sessionId, 'sessionId');
|
|
87
|
+
requireTransferIdentifier(sessionId, 'sessionId');
|
|
88
|
+
if (options.title !== undefined) {
|
|
89
|
+
validateUtf8String(
|
|
90
|
+
options.title,
|
|
91
|
+
'title',
|
|
92
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes,
|
|
93
|
+
true,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
await resolved.state.scopeQueue;
|
|
97
|
+
if (
|
|
98
|
+
resolved.state.sessions.has(sessionId)
|
|
99
|
+
|| resolved.state.tombstones.has(sessionId)
|
|
100
|
+
|| resolved.state.initializingSessions.has(sessionId)
|
|
101
|
+
) {
|
|
102
|
+
throw new FlexHarnessValidationError(`Session "${sessionId}" already exists or is being deleted.`);
|
|
103
|
+
}
|
|
104
|
+
resolved.state.initializingSessions.add(sessionId);
|
|
105
|
+
let resolveInitialization!: () => void;
|
|
106
|
+
const initializationCompletion = new Promise<void>((resolve) => {
|
|
107
|
+
resolveInitialization = resolve;
|
|
108
|
+
});
|
|
109
|
+
let initializationCompleted = false;
|
|
110
|
+
const completeInitialization = () => {
|
|
111
|
+
if (initializationCompleted) return;
|
|
112
|
+
initializationCompleted = true;
|
|
113
|
+
if (resolved.state.sessionInitializations.get(sessionId) === initializationCompletion) {
|
|
114
|
+
resolved.state.sessionInitializations.delete(sessionId);
|
|
115
|
+
}
|
|
116
|
+
resolveInitialization();
|
|
117
|
+
};
|
|
118
|
+
resolved.state.sessionInitializations.set(sessionId, initializationCompletion);
|
|
119
|
+
let scopeReserved = false;
|
|
120
|
+
let metadata!: IFlexSession;
|
|
121
|
+
try {
|
|
122
|
+
await this.core.mutateScope(resolved.state, () => {
|
|
123
|
+
if (resolved.state.sessions.has(sessionId) || resolved.state.tombstones.has(sessionId)) {
|
|
124
|
+
throw new FlexHarnessValidationError(`Session "${sessionId}" already exists or is being deleted.`);
|
|
125
|
+
}
|
|
126
|
+
const timestamp = new Date().toISOString();
|
|
127
|
+
metadata = {
|
|
128
|
+
scopeId,
|
|
129
|
+
sessionId,
|
|
130
|
+
sessionGenerationId: requestedSessionGenerationId ?? createSessionGenerationId(),
|
|
131
|
+
sessionGenerationSequence: resolved.state.revision + 1,
|
|
132
|
+
...(options.configurationRef === undefined ? {} : { configurationRef: options.configurationRef }),
|
|
133
|
+
...(options.title ? { title: options.title } : {}),
|
|
134
|
+
createdAt: timestamp,
|
|
135
|
+
updatedAt: timestamp,
|
|
136
|
+
status: 'idle',
|
|
137
|
+
activity: { status: 'idle' },
|
|
138
|
+
depth: 0,
|
|
139
|
+
};
|
|
140
|
+
resolved.state.sessions.set(
|
|
141
|
+
sessionId,
|
|
142
|
+
this.core.createUninitializedStoredSession(metadata, resolved.state.storageKey),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
scopeReserved = true;
|
|
146
|
+
const placeholder = resolved.state.sessions.get(sessionId)!;
|
|
147
|
+
const initialization = this.core.loadSessionRuntime(
|
|
148
|
+
resolved.state,
|
|
149
|
+
metadata,
|
|
150
|
+
scopeId,
|
|
151
|
+
resolved.scope.scope,
|
|
152
|
+
placeholder.compactorLifecycleController,
|
|
153
|
+
true,
|
|
154
|
+
);
|
|
155
|
+
const loaded = await initialization;
|
|
156
|
+
if (resolved.state.sessions.get(sessionId) !== placeholder) {
|
|
157
|
+
const aborted = new FlexHarnessAbortError('The session was deleted while it was being created.');
|
|
158
|
+
try {
|
|
159
|
+
await this.core.lifecycle.closeStoredSession(loaded);
|
|
160
|
+
} catch (closeError) {
|
|
161
|
+
this.core.lifecycle.orphanedStoredSessions.add(loaded);
|
|
162
|
+
throw combineErrors([aborted, closeError]);
|
|
163
|
+
}
|
|
164
|
+
throw aborted;
|
|
165
|
+
}
|
|
166
|
+
resolved.state.sessions.set(sessionId, loaded);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const projectedError = this.core.projectOperationError(
|
|
169
|
+
error,
|
|
170
|
+
'agentSession',
|
|
171
|
+
scopeId,
|
|
172
|
+
sessionId,
|
|
173
|
+
'session-create',
|
|
174
|
+
);
|
|
175
|
+
scopeReserved ||= resolved.state.sessions.has(sessionId);
|
|
176
|
+
if (!scopeReserved) throw projectedError;
|
|
177
|
+
const tombstone: IFlexSessionTombstone = {
|
|
178
|
+
sessionId,
|
|
179
|
+
...requireSessionGeneration(metadata),
|
|
180
|
+
deletedAt: new Date().toISOString(),
|
|
181
|
+
rootSessionId: sessionId,
|
|
182
|
+
depth: 0,
|
|
183
|
+
};
|
|
184
|
+
let tombstoneCommitted = false;
|
|
185
|
+
let domainCleanupCompleted = false;
|
|
186
|
+
try {
|
|
187
|
+
await this.core.mutateScope(resolved.state, () => {
|
|
188
|
+
resolved.state.sessions.delete(sessionId);
|
|
189
|
+
resolved.state.tombstones.set(sessionId, tombstone);
|
|
190
|
+
}, true);
|
|
191
|
+
tombstoneCommitted = true;
|
|
192
|
+
completeInitialization();
|
|
193
|
+
await this.core.deletion.finishTombstoneCleanup(
|
|
194
|
+
resolved.state,
|
|
195
|
+
sessionId,
|
|
196
|
+
scopeId,
|
|
197
|
+
{ scopeId, scope: resolved.scope.scope },
|
|
198
|
+
);
|
|
199
|
+
domainCleanupCompleted = true;
|
|
200
|
+
} catch (cleanupError) {
|
|
201
|
+
const projectedCleanup = this.core.projectOperationError(
|
|
202
|
+
cleanupError,
|
|
203
|
+
'persistence',
|
|
204
|
+
scopeId,
|
|
205
|
+
sessionId,
|
|
206
|
+
'session-create-cleanup',
|
|
207
|
+
);
|
|
208
|
+
if (!tombstoneCommitted || domainCleanupCompleted) {
|
|
209
|
+
resolved.state.sessions.delete(sessionId);
|
|
210
|
+
const stateLoad = this.core.stateLoads.get(resolved.state.storageKey);
|
|
211
|
+
if (stateLoad) {
|
|
212
|
+
completeInitialization();
|
|
213
|
+
try {
|
|
214
|
+
await this.core.lifecycle.drainStorage(
|
|
215
|
+
resolved.state.storageKey,
|
|
216
|
+
stateLoad,
|
|
217
|
+
this.core.trustInternalError(new FlexHarnessAbortError(
|
|
218
|
+
'The session namespace was fenced after uncertain creation cleanup.',
|
|
219
|
+
)),
|
|
220
|
+
{ scopeId, scope: resolved.scope.scope },
|
|
221
|
+
);
|
|
222
|
+
} catch {
|
|
223
|
+
// The projected creation and cleanup errors remain safe for the caller.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw combineErrors([projectedError, projectedCleanup]);
|
|
228
|
+
}
|
|
229
|
+
throw projectedError;
|
|
230
|
+
} finally {
|
|
231
|
+
resolved.state.initializingSessions.delete(sessionId);
|
|
232
|
+
completeInitialization();
|
|
233
|
+
}
|
|
234
|
+
const result = publicSnapshot(metadata);
|
|
235
|
+
this.core.emitEvent(scopeId, metadata, { type: 'session.created', session: result });
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public async getSession(scopeId: string, sessionId: string): Promise<IFlexSession> {
|
|
240
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
241
|
+
await state.scopeQueue;
|
|
242
|
+
this.core.assertStateAcceptingWork(state);
|
|
243
|
+
return publicSnapshot(this.core.requireSession(state, sessionId).session);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public async getSessionMetrics(scopeId: string, sessionId: string): Promise<IFlexSessionMetrics> {
|
|
247
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
248
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
249
|
+
await stored.projectionQueue;
|
|
250
|
+
this.core.assertStateAcceptingWork(state);
|
|
251
|
+
if (stored.metricsFailure !== undefined) throw stored.metricsFailure;
|
|
252
|
+
return publicSnapshot(sessionMetricsView(stored.metrics));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
public async updateSession(
|
|
256
|
+
scopeId: string,
|
|
257
|
+
sessionId: string,
|
|
258
|
+
options: IFlexUpdateSessionOptions,
|
|
259
|
+
): Promise<IFlexSession> {
|
|
260
|
+
validateUpdateSessionOptions(options);
|
|
261
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
262
|
+
let result!: IFlexSession;
|
|
263
|
+
await this.core.mutateScope(state, () => {
|
|
264
|
+
const stored = Object.prototype.hasOwnProperty.call(options, 'archived')
|
|
265
|
+
? this.core.requireMutableSession(state, sessionId)
|
|
266
|
+
: this.core.requireSession(state, sessionId);
|
|
267
|
+
const timestamp = new Date().toISOString();
|
|
268
|
+
if (Object.prototype.hasOwnProperty.call(options, 'title')) {
|
|
269
|
+
if (options.title === null) delete stored.session.title;
|
|
270
|
+
else stored.session.title = options.title!;
|
|
271
|
+
}
|
|
272
|
+
if (options.archived === true) stored.session.archivedAt ??= timestamp;
|
|
273
|
+
else if (options.archived === false) delete stored.session.archivedAt;
|
|
274
|
+
stored.session.updatedAt = timestamp;
|
|
275
|
+
result = publicSnapshot(stored.session);
|
|
276
|
+
});
|
|
277
|
+
this.core.emitEvent(scopeId, result, { type: 'session.updated', session: result });
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
public async getSessionView(scopeId: string, sessionId: string): Promise<IFlexSessionView> {
|
|
282
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
283
|
+
let stored = this.core.requireSession(state, sessionId);
|
|
284
|
+
for (let pass = 0; ; pass++) {
|
|
285
|
+
if (pass === maxSessionViewPasses) {
|
|
286
|
+
throw new FlexHarnessSessionBusyError(
|
|
287
|
+
sessionId,
|
|
288
|
+
`kept changing through ${maxSessionViewPasses} consecutive waits for a settled view`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const scopeQueue = state.scopeQueue;
|
|
292
|
+
const projectionQueue = stored.projectionQueue;
|
|
293
|
+
const publications = [...(state.publications.get(sessionId) ?? [])];
|
|
294
|
+
await Promise.all([scopeQueue, projectionQueue, ...publications]);
|
|
295
|
+
this.core.assertStateAcceptingWork(state);
|
|
296
|
+
stored = this.core.requireSession(state, sessionId);
|
|
297
|
+
if (
|
|
298
|
+
state.scopeQueue === scopeQueue
|
|
299
|
+
&& stored.projectionQueue === projectionQueue
|
|
300
|
+
&& !state.publications.get(sessionId)?.size
|
|
301
|
+
) break;
|
|
302
|
+
}
|
|
303
|
+
const run = state.activeRuns.get(sessionId);
|
|
304
|
+
const messages = this.core.visibleMessages(stored).map((message) => (
|
|
305
|
+
run && run.stored === stored && message.messageId === run.assistantMessageId
|
|
306
|
+
&& message.status === 'streaming'
|
|
307
|
+
? { ...message, parts: run.callbackParts }
|
|
308
|
+
: message
|
|
309
|
+
));
|
|
310
|
+
return publicSnapshot({
|
|
311
|
+
session: stored.session,
|
|
312
|
+
messages,
|
|
313
|
+
pendingPermissions: [...state.pendingPermissions.values()]
|
|
314
|
+
.map((pending) => pending.request)
|
|
315
|
+
.filter((request) => request.sessionId === sessionId)
|
|
316
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt)),
|
|
317
|
+
sequence: this.core.sequence,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
public async getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
|
|
322
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
323
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
324
|
+
await stored.projectionQueue;
|
|
325
|
+
this.core.assertStateAcceptingWork(state);
|
|
326
|
+
return publicSnapshot(this.core.visibleMessages(stored));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
public async listMessagePage(
|
|
330
|
+
scopeId: string,
|
|
331
|
+
sessionId: string,
|
|
332
|
+
options: IFlexMessagePageOptions = {},
|
|
333
|
+
): Promise<IFlexMessagePage> {
|
|
334
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
335
|
+
throw new FlexHarnessValidationError('Message page options must be a plain object.');
|
|
336
|
+
}
|
|
337
|
+
const unsupported = Object.keys(options).find((key) => key !== 'limit' && key !== 'before');
|
|
338
|
+
if (unsupported) throw new FlexHarnessValidationError(`Message page options do not support "${unsupported}".`);
|
|
339
|
+
const limit = options.limit ?? maxMessagePageSize;
|
|
340
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > maxMessagePageSize) {
|
|
341
|
+
throw new FlexHarnessValidationError(`Message page limit must be an integer from 1 through ${maxMessagePageSize}.`);
|
|
342
|
+
}
|
|
343
|
+
if (
|
|
344
|
+
options.before !== undefined
|
|
345
|
+
&& (typeof options.before !== 'string'
|
|
346
|
+
|| !options.before
|
|
347
|
+
|| Buffer.byteLength(options.before, 'utf8') > maxMessagePageCursorBytes)
|
|
348
|
+
) {
|
|
349
|
+
throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
350
|
+
}
|
|
351
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
352
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
353
|
+
await stored.projectionQueue;
|
|
354
|
+
this.core.assertStateAcceptingWork(state);
|
|
355
|
+
const namespace = this.core.messageCursorNamespace(state.storageKey);
|
|
356
|
+
const visibleMessages = this.core.visibleMessages(stored);
|
|
357
|
+
let end = visibleMessages.length;
|
|
358
|
+
if (options.before !== undefined) {
|
|
359
|
+
const cursor = this.core.parseMessageCursor(options.before);
|
|
360
|
+
if (cursor.namespace !== namespace || cursor.sessionId !== sessionId) {
|
|
361
|
+
throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
362
|
+
}
|
|
363
|
+
const anchor = visibleMessages.findIndex((message) => message.messageId === cursor.anchorMessageId);
|
|
364
|
+
if (anchor < 0) throw new FlexHarnessValidationError('Message page cursor is stale.');
|
|
365
|
+
end = anchor;
|
|
366
|
+
}
|
|
367
|
+
let start = end;
|
|
368
|
+
let messages: IFlexMessage[] = [];
|
|
369
|
+
for (let index = end - 1; index >= 0 && messages.length < limit; index--) {
|
|
370
|
+
const candidate = createBoundedTransferMessage(visibleMessages[index]);
|
|
371
|
+
const candidateMessages = [candidate, ...messages];
|
|
372
|
+
const nextCursor = index > 0
|
|
373
|
+
? this.core.createMessageCursor(namespace, sessionId, candidate.messageId)
|
|
374
|
+
: undefined;
|
|
375
|
+
const candidatePage: IFlexMessagePage = {
|
|
376
|
+
messages: candidateMessages,
|
|
377
|
+
startIndex: index,
|
|
378
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
379
|
+
};
|
|
380
|
+
if (jsonBytes(candidatePage) > maxTransferPageBytes) break;
|
|
381
|
+
start = index;
|
|
382
|
+
messages = candidateMessages;
|
|
383
|
+
}
|
|
384
|
+
if (messages.length === 0 && end > 0) {
|
|
385
|
+
throw new FlexHarnessValidationError('Stored message exceeds the page transfer limit.');
|
|
386
|
+
}
|
|
387
|
+
const nextCursor = start > 0
|
|
388
|
+
? this.core.createMessageCursor(namespace, sessionId, messages[0].messageId)
|
|
389
|
+
: undefined;
|
|
390
|
+
return publicSnapshot({ messages, startIndex: start, ...(nextCursor ? { nextCursor } : {}) });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
public async getMessage(
|
|
394
|
+
scopeId: string,
|
|
395
|
+
sessionId: string,
|
|
396
|
+
messageId: string,
|
|
397
|
+
): Promise<IFlexMessage> {
|
|
398
|
+
validateIdentifier(messageId, 'messageId');
|
|
399
|
+
requireTransferIdentifier(messageId, 'messageId');
|
|
400
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
401
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
402
|
+
await stored.projectionQueue;
|
|
403
|
+
this.core.assertStateAcceptingWork(state);
|
|
404
|
+
return publicSnapshot(createBoundedTransferMessage(this.core.requireMessage(stored, messageId)));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
public async pushRuntimeEvent(
|
|
408
|
+
scopeId: string,
|
|
409
|
+
sessionId: string,
|
|
410
|
+
event: IJsonObject & { type: string },
|
|
411
|
+
): Promise<void> {
|
|
412
|
+
this.core.validateRuntimeEvent(event);
|
|
413
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
414
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
415
|
+
this.core.assertSessionWritable(stored);
|
|
416
|
+
this.core.assertStateAcceptingWork(state);
|
|
417
|
+
try {
|
|
418
|
+
await this.core.compaction.withCompactorContext(
|
|
419
|
+
this.core.compaction.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId),
|
|
420
|
+
() => stored.agentSession.pushRuntimeEvent(cloneSerializable(event)),
|
|
421
|
+
);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
throw this.core.projectOperationError(error, 'agentSession', scopeId, sessionId, 'runtime-event');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
public async archiveSessionEvents(
|
|
428
|
+
scopeId: string,
|
|
429
|
+
sessionId: string,
|
|
430
|
+
compactionEventId?: string,
|
|
431
|
+
): Promise<IFlexEventArchiveMetadata | undefined> {
|
|
432
|
+
if (compactionEventId !== undefined) {
|
|
433
|
+
validateIdentifier(compactionEventId, 'compactionEventId');
|
|
434
|
+
requireTransferIdentifier(compactionEventId, 'compactionEventId');
|
|
435
|
+
}
|
|
436
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
437
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
438
|
+
this.core.assertSessionWritable(stored);
|
|
439
|
+
this.core.assertStateAcceptingWork(state);
|
|
440
|
+
this.core.commands.requireSessionAvailableForSlashCommand(state, stored, 'archive');
|
|
441
|
+
const compaction = compactionEventId === undefined
|
|
442
|
+
? [...stored.agentSession.getEvents()].reverse().find((event) =>
|
|
443
|
+
event.type === 'context-compaction')
|
|
444
|
+
: stored.agentSession.getEvents().find((event) =>
|
|
445
|
+
event.type === 'context-compaction' && event.id === compactionEventId);
|
|
446
|
+
if (!compaction || compaction.type !== 'context-compaction') return undefined;
|
|
447
|
+
let archive;
|
|
448
|
+
try {
|
|
449
|
+
archive = await stored.agentSession.archiveCompactedEvents(compaction.id);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
throw this.core.projectOperationError(error, 'agentSession', scopeId, sessionId, 'archive');
|
|
452
|
+
}
|
|
453
|
+
if (!archive) return undefined;
|
|
454
|
+
let branchCommitted = false;
|
|
455
|
+
try {
|
|
456
|
+
const archivedIds = new Set(archive.events.map((event) => event.id));
|
|
457
|
+
await this.core.mutateProjection(state, stored, () => {
|
|
458
|
+
branchCommitted = this.core.reversions.commitRevertedBranchState(stored);
|
|
459
|
+
for (const segment of stored.reversionSegments) {
|
|
460
|
+
if (segment.eventIds.some((eventId) => archivedIds.has(eventId))) {
|
|
461
|
+
segment.contextAvailable = false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
this.core.reversions.pruneReversionState(stored);
|
|
465
|
+
}, true);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
throw this.core.projectOperationError(error, 'persistence', scopeId, sessionId, 'archive-horizon');
|
|
468
|
+
}
|
|
469
|
+
if (branchCommitted) {
|
|
470
|
+
this.core.emitEvent(scopeId, stored.session, {
|
|
471
|
+
type: 'session.history.changed',
|
|
472
|
+
direction: 'branch',
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
await this.core.reversions.drainReversionReleases(state, stored, scopeId, scope.scope, false);
|
|
476
|
+
return publicSnapshot({
|
|
477
|
+
archiveId: this.core.boundedIdentifier(archive.archiveId, 'archiveId'),
|
|
478
|
+
sessionId: this.core.boundedIdentifier(archive.sessionId, 'sessionId'),
|
|
479
|
+
createdAt: new Date(archive.createdAt).toISOString(),
|
|
480
|
+
eventCount: archive.events.length,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|