@modelprofile.com/flexharness 13.1.0 → 13.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.cachewarm.d.ts +64 -0
- package/dist_ts/classes.flexharness.cachewarm.js +353 -0
- package/dist_ts/classes.flexharness.callbacks.js +2 -1
- package/dist_ts/classes.flexharness.core.d.ts +4 -1
- package/dist_ts/classes.flexharness.core.js +9 -3
- package/dist_ts/classes.flexharness.d.ts +10 -1
- package/dist_ts/classes.flexharness.deletion.js +2 -1
- package/dist_ts/classes.flexharness.js +14 -1
- package/dist_ts/classes.flexharness.lifecycle.js +4 -1
- package/dist_ts/classes.flexharness.prompts.d.ts +1 -1
- package/dist_ts/classes.flexharness.prompts.js +28 -9
- package/dist_ts/classes.flexharness.reversions.d.ts +2 -0
- package/dist_ts/classes.flexharness.reversions.js +7 -2
- package/dist_ts/classes.flexharness.runs.js +25 -3
- package/dist_ts/classes.toolcatalog.d.ts +10 -2
- package/dist_ts/classes.toolcatalog.js +73 -7
- package/dist_ts/interfaces.d.ts +89 -1
- package/dist_ts/interfaces.flexharness.d.ts +15 -0
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +1 -1
- package/dist_ts/plugins.js +1 -1
- package/dist_ts/utils.harnessoptions.d.ts +8 -1
- package/dist_ts/utils.harnessoptions.js +50 -6
- package/dist_ts/utils.json.js +9 -2
- package/dist_ts/utils.sessionpreview.d.ts +3 -3
- package/dist_ts/utils.sessionpreview.js +5 -5
- package/dist_ts_chat_harness/classes.transcript.js +17 -4
- package/dist_ts_chat_harness/functions.mapping.d.ts +1 -0
- package/dist_ts_chat_harness/functions.mapping.js +5 -2
- package/package.json +3 -3
- package/readme.md +27 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.cachewarm.ts +433 -0
- package/ts/classes.flexharness.callbacks.ts +1 -0
- package/ts/classes.flexharness.core.ts +8 -1
- package/ts/classes.flexharness.deletion.ts +1 -0
- package/ts/classes.flexharness.lifecycle.ts +3 -0
- package/ts/classes.flexharness.prompts.ts +25 -6
- package/ts/classes.flexharness.reversions.ts +7 -1
- package/ts/classes.flexharness.runs.ts +20 -2
- package/ts/classes.flexharness.ts +21 -0
- package/ts/classes.toolcatalog.ts +76 -7
- package/ts/interfaces.flexharness.ts +16 -0
- package/ts/interfaces.ts +100 -1
- package/ts/plugins.ts +1 -0
- package/ts/readme.md +27 -0
- package/ts/utils.harnessoptions.ts +74 -5
- package/ts/utils.json.ts +8 -1
- package/ts/utils.sessionpreview.ts +4 -4
- package/ts_chat_harness/classes.transcript.ts +14 -3
- package/ts_chat_harness/functions.mapping.ts +3 -1
- package/ts_chat_harness/readme.md +1 -0
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import type {
|
|
13
13
|
IFlexAgentSessionPolicy,
|
|
14
14
|
IFlexAttachmentResolver,
|
|
15
|
+
IFlexCacheWarmClock,
|
|
16
|
+
IFlexCacheWarmOptions,
|
|
15
17
|
IFlexCallbackLimits,
|
|
16
18
|
IFlexHarnessOptions,
|
|
17
19
|
IFlexHarnessStores,
|
|
@@ -46,8 +48,21 @@ export interface INormalizedProjectManagementTools {
|
|
|
46
48
|
export interface INormalizedBuiltInTools {
|
|
47
49
|
readonly renameSession: boolean;
|
|
48
50
|
readonly projectManagement?: INormalizedProjectManagementTools;
|
|
51
|
+
readonly cacheWarm: boolean;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
export interface INormalizedCacheWarmOptions {
|
|
55
|
+
readonly maxDurationMs: number;
|
|
56
|
+
readonly maxOutputTokens: number;
|
|
57
|
+
readonly clock: IFlexCacheWarmClock;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const defaultCacheWarmMaxDurationMs = 4 * 60 * 60 * 1000;
|
|
61
|
+
// One call warms for at most a day; a host extends it by calling again.
|
|
62
|
+
const maximumCacheWarmMaxDurationMs = 24 * 60 * 60 * 1000;
|
|
63
|
+
const defaultCacheWarmMaxOutputTokens = 1024;
|
|
64
|
+
const maximumCacheWarmMaxOutputTokens = 32_768;
|
|
65
|
+
|
|
51
66
|
const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
|
|
52
67
|
maxEvents: 10_000,
|
|
53
68
|
maxOutputBytes: 1024 * 1024,
|
|
@@ -242,7 +257,7 @@ function normalizeCodeModeLimits(codeMode: unknown): Readonly<IFlexCodeModeLimit
|
|
|
242
257
|
export function normalizeBuiltInTools(
|
|
243
258
|
options: IFlexHarnessOptions<unknown>['builtInTools'],
|
|
244
259
|
): INormalizedBuiltInTools {
|
|
245
|
-
if (options === undefined) return Object.freeze({ renameSession: false });
|
|
260
|
+
if (options === undefined) return Object.freeze({ renameSession: false, cacheWarm: false });
|
|
246
261
|
if (
|
|
247
262
|
!options
|
|
248
263
|
|| typeof options !== 'object'
|
|
@@ -252,14 +267,19 @@ export function normalizeBuiltInTools(
|
|
|
252
267
|
throw new FlexHarnessValidationError('builtInTools must be a plain object.');
|
|
253
268
|
}
|
|
254
269
|
const unsupported = Object.keys(options)
|
|
255
|
-
.find((key) => !['renameSession', 'projectManagement'].includes(key));
|
|
270
|
+
.find((key) => !['renameSession', 'projectManagement', 'cacheWarm'].includes(key));
|
|
256
271
|
if (unsupported) throw new FlexHarnessValidationError(`builtInTools does not support "${unsupported}".`);
|
|
257
|
-
|
|
258
|
-
|
|
272
|
+
for (const key of ['renameSession', 'cacheWarm'] as const) {
|
|
273
|
+
if (options[key] !== undefined && typeof options[key] !== 'boolean') {
|
|
274
|
+
throw new FlexHarnessValidationError(`builtInTools.${key} must be a boolean.`);
|
|
275
|
+
}
|
|
259
276
|
}
|
|
260
277
|
const projectManagement = options.projectManagement;
|
|
261
278
|
if (projectManagement === undefined) {
|
|
262
|
-
return Object.freeze({
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
renameSession: options.renameSession ?? false,
|
|
281
|
+
cacheWarm: options.cacheWarm ?? false,
|
|
282
|
+
});
|
|
263
283
|
}
|
|
264
284
|
if (
|
|
265
285
|
!projectManagement
|
|
@@ -283,6 +303,7 @@ export function normalizeBuiltInTools(
|
|
|
283
303
|
}
|
|
284
304
|
return Object.freeze({
|
|
285
305
|
renameSession: options.renameSession ?? false,
|
|
306
|
+
cacheWarm: options.cacheWarm ?? false,
|
|
286
307
|
projectManagement: Object.freeze({
|
|
287
308
|
task: projectManagement.task ?? true,
|
|
288
309
|
goal: projectManagement.goal ?? true,
|
|
@@ -374,6 +395,54 @@ export function normalizeSlashCommands<TScope>(
|
|
|
374
395
|
return Object.freeze(normalized);
|
|
375
396
|
}
|
|
376
397
|
|
|
398
|
+
const systemCacheWarmClock: IFlexCacheWarmClock = Object.freeze({
|
|
399
|
+
now: () => Date.now(),
|
|
400
|
+
setTimeout: (callback: () => void, delayMs: number) => {
|
|
401
|
+
const handle = setTimeout(callback, delayMs);
|
|
402
|
+
// A warm period never holds the process open.
|
|
403
|
+
handle.unref?.();
|
|
404
|
+
return handle;
|
|
405
|
+
},
|
|
406
|
+
clearTimeout: (handle: unknown) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
export function normalizeCacheWarmOptions(
|
|
410
|
+
input: IFlexHarnessOptions<unknown>['cacheWarm'],
|
|
411
|
+
): INormalizedCacheWarmOptions {
|
|
412
|
+
if (input !== undefined && !isPlainOptions(input as unknown)) {
|
|
413
|
+
throw new FlexHarnessValidationError('cacheWarm must be a plain object.');
|
|
414
|
+
}
|
|
415
|
+
const options: IFlexCacheWarmOptions = input ?? {};
|
|
416
|
+
const unsupported = Object.keys(options)
|
|
417
|
+
.find((key) => !['maxDurationMs', 'maxOutputTokens', 'clock'].includes(key));
|
|
418
|
+
if (unsupported) throw new FlexHarnessValidationError(`cacheWarm does not support "${unsupported}".`);
|
|
419
|
+
const clock: IFlexCacheWarmClock = options.clock ?? systemCacheWarmClock;
|
|
420
|
+
if (
|
|
421
|
+
!clock
|
|
422
|
+
|| typeof clock !== 'object'
|
|
423
|
+
|| typeof clock.now !== 'function'
|
|
424
|
+
|| typeof clock.setTimeout !== 'function'
|
|
425
|
+
|| typeof clock.clearTimeout !== 'function'
|
|
426
|
+
) {
|
|
427
|
+
throw new FlexHarnessValidationError('cacheWarm.clock must provide now, setTimeout and clearTimeout.');
|
|
428
|
+
}
|
|
429
|
+
return Object.freeze({
|
|
430
|
+
maxDurationMs: resolveBoundedPositiveInteger(
|
|
431
|
+
options.maxDurationMs,
|
|
432
|
+
'cacheWarm.maxDurationMs',
|
|
433
|
+
defaultCacheWarmMaxDurationMs,
|
|
434
|
+
maximumCacheWarmMaxDurationMs,
|
|
435
|
+
),
|
|
436
|
+
maxOutputTokens: resolveBoundedPositiveInteger(
|
|
437
|
+
options.maxOutputTokens,
|
|
438
|
+
'cacheWarm.maxOutputTokens',
|
|
439
|
+
defaultCacheWarmMaxOutputTokens,
|
|
440
|
+
maximumCacheWarmMaxOutputTokens,
|
|
441
|
+
),
|
|
442
|
+
clock,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
377
446
|
export function resolveBoundedPositiveInteger(
|
|
378
447
|
value: number | undefined,
|
|
379
448
|
name: string,
|
package/ts/utils.json.ts
CHANGED
|
@@ -566,7 +566,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
566
566
|
const activity = requireRecord(session.activity, `${path}.activity`);
|
|
567
567
|
requireOnlyKeys(
|
|
568
568
|
activity,
|
|
569
|
-
['runId', 'status', 'startedAt', 'completedAt', 'error', 'limit', 'rejectedPermissionId'],
|
|
569
|
+
['runId', 'status', 'startedAt', 'completedAt', 'error', 'limit', 'rejectedPermissionId', 'cacheWarm'],
|
|
570
570
|
`${path}.activity`,
|
|
571
571
|
);
|
|
572
572
|
if (!activityStatuses.includes(activity.status as (typeof activityStatuses)[number])) {
|
|
@@ -578,6 +578,9 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
578
578
|
requireOptionalString(activity.error, `${path}.activity.error`);
|
|
579
579
|
requireOptionalModelLimit(activity.limit, `${path}.activity.limit`);
|
|
580
580
|
requireOptionalString(activity.rejectedPermissionId, `${path}.activity.rejectedPermissionId`);
|
|
581
|
+
if (activity.cacheWarm !== undefined && activity.cacheWarm !== true) {
|
|
582
|
+
throw new FlexHarnessStoreFormatError(`${path}.activity.cacheWarm must be true when present.`);
|
|
583
|
+
}
|
|
581
584
|
const relationship = [
|
|
582
585
|
session.parentSessionId,
|
|
583
586
|
session.parentRunId,
|
|
@@ -776,6 +779,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
776
779
|
'limit',
|
|
777
780
|
'rejectedPermissionId',
|
|
778
781
|
'steerId',
|
|
782
|
+
'cacheWarm',
|
|
779
783
|
],
|
|
780
784
|
path,
|
|
781
785
|
);
|
|
@@ -799,6 +803,9 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
799
803
|
throw new FlexHarnessStoreFormatError(`${path}.steerId is only valid on a user message.`);
|
|
800
804
|
}
|
|
801
805
|
}
|
|
806
|
+
if (message.cacheWarm !== undefined && message.cacheWarm !== true) {
|
|
807
|
+
throw new FlexHarnessStoreFormatError(`${path}.cacheWarm must be true when present.`);
|
|
808
|
+
}
|
|
802
809
|
if (message.model !== undefined) validateModel(message.model, `${path}.model`);
|
|
803
810
|
if (message.usage !== undefined) validateUsage(message.usage, `${path}.usage`);
|
|
804
811
|
if (!Array.isArray(message.parts)) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { FLEX_PROJECT_MANAGEMENT_LIMITS, type IFlexMessage } from './interfaces.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* What a session is about before anyone titles it: the text of its first prompt that has any,
|
|
5
|
-
* one line (every run of whitespace becomes one space), cut to the
|
|
6
|
-
* boundary. Undefined while no prompt carries text.
|
|
4
|
+
* What a session is about before anyone titles it: the text of its first prompt that has any,
|
|
5
|
+
* cache-warm prompts aside, as one line (every run of whitespace becomes one space), cut to the
|
|
6
|
+
* title bound at a character boundary. Undefined while no prompt carries text.
|
|
7
7
|
*/
|
|
8
8
|
export function sessionPreview(messagesArg: readonly IFlexMessage[]): string | undefined {
|
|
9
9
|
for (const message of messagesArg) {
|
|
10
|
-
if (message.role !== 'user') continue;
|
|
10
|
+
if (message.role !== 'user' || message.cacheWarm) continue;
|
|
11
11
|
const text = message.parts
|
|
12
12
|
.map((part) => (part.type === 'text' ? part.text : ''))
|
|
13
13
|
.join(' ')
|
|
@@ -180,7 +180,7 @@ export class FlexHarnessTranscript {
|
|
|
180
180
|
public view(): IFlexHarnessTranscriptView {
|
|
181
181
|
return structuredClone({
|
|
182
182
|
messages: this.flexMessages.flatMap((message, index) =>
|
|
183
|
-
flexMessageToHarnessRows(message, index, this.timings)),
|
|
183
|
+
message.cacheWarm ? [] : flexMessageToHarnessRows(message, index, this.timings)),
|
|
184
184
|
permissions: [...this.permissionRows.values()],
|
|
185
185
|
questions: [],
|
|
186
186
|
status: this.status,
|
|
@@ -217,6 +217,11 @@ export class FlexHarnessTranscript {
|
|
|
217
217
|
}
|
|
218
218
|
if (event.scopeId !== this.scopeId || event.sessionId !== this.sessionId) return change;
|
|
219
219
|
const timestamp = Date.parse(event.timestamp);
|
|
220
|
+
// A cache-warm turn's messages are held but not shown, so their events change no row.
|
|
221
|
+
const warmMessage = 'message' in event
|
|
222
|
+
? event.message.cacheWarm === true
|
|
223
|
+
: 'messageId' in event && this.flexMessages.some((message) =>
|
|
224
|
+
message.messageId === event.messageId && message.cacheWarm);
|
|
220
225
|
switch (event.type) {
|
|
221
226
|
case 'session.created':
|
|
222
227
|
case 'session.updated':
|
|
@@ -227,7 +232,7 @@ export class FlexHarnessTranscript {
|
|
|
227
232
|
change.messages = change.permissions = change.status = change.usage = true;
|
|
228
233
|
break;
|
|
229
234
|
case 'run.started':
|
|
230
|
-
this.setStatus(change, { type: 'busy' });
|
|
235
|
+
if (!event.cacheWarm) this.setStatus(change, { type: 'busy' });
|
|
231
236
|
break;
|
|
232
237
|
case 'message.created':
|
|
233
238
|
case 'message.updated':
|
|
@@ -280,13 +285,15 @@ export class FlexHarnessTranscript {
|
|
|
280
285
|
this.usage = sumUsage(this.usage, flexUsageToHarness(event.message.usage));
|
|
281
286
|
change.usage = true;
|
|
282
287
|
}
|
|
288
|
+
if (event.cacheWarm) break;
|
|
283
289
|
this.setStatus(change, event.status === 'failed'
|
|
284
290
|
? { type: 'error', ...(event.error ? { message: event.error.message } : {}) }
|
|
285
291
|
: { type: 'idle' });
|
|
286
292
|
break;
|
|
287
293
|
case 'prompt.started':
|
|
288
294
|
case 'prompt.running':
|
|
289
|
-
|
|
295
|
+
// A steer cannot join a cache-warm turn.
|
|
296
|
+
if (!event.entry.cacheWarm) this.runningQueueId = event.queueId;
|
|
290
297
|
break;
|
|
291
298
|
case 'prompt.finished':
|
|
292
299
|
if (this.runningQueueId === event.queueId) this.runningQueueId = undefined;
|
|
@@ -297,6 +304,10 @@ export class FlexHarnessTranscript {
|
|
|
297
304
|
default:
|
|
298
305
|
break;
|
|
299
306
|
}
|
|
307
|
+
if (warmMessage) {
|
|
308
|
+
change.messages = false;
|
|
309
|
+
change.deltas = [];
|
|
310
|
+
}
|
|
300
311
|
return change;
|
|
301
312
|
}
|
|
302
313
|
|
|
@@ -215,7 +215,9 @@ export function flexPermissionToHarness(
|
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/** A cache-warm turn is idle time to the user: its activity shows as idle. */
|
|
218
219
|
export function flexSessionToHarnessStatus(session: IFlexSession): plugins.IHarnessStatus {
|
|
220
|
+
if (session.activity.cacheWarm) return { type: 'idle' };
|
|
219
221
|
switch (session.activity.status) {
|
|
220
222
|
case 'running':
|
|
221
223
|
case 'scheduled':
|
|
@@ -247,7 +249,7 @@ export function flexSessionToHarnessMeta(session: IFlexSession): plugins.IHarnes
|
|
|
247
249
|
title: session.title ?? session.sessionId,
|
|
248
250
|
...(epoch(session.createdAt) === undefined ? {} : { createdAt: epoch(session.createdAt) }),
|
|
249
251
|
...(epoch(session.updatedAt) === undefined ? {} : { updatedAt: epoch(session.updatedAt) }),
|
|
250
|
-
state: sessionCardStates[session.activity.status],
|
|
252
|
+
state: session.activity.cacheWarm ? 'normal' : sessionCardStates[session.activity.status],
|
|
251
253
|
};
|
|
252
254
|
}
|
|
253
255
|
|
|
@@ -78,6 +78,7 @@ The host forwards every event of the harness instance, of every session, in orde
|
|
|
78
78
|
- Permission rows stay in the transcript once answered, showing the decision. A request its run leaves unanswered, because the run was stopped or failed, is removed when the run finishes.
|
|
79
79
|
- FlexHarness emits no event for project state. After a run, or after a project tool's row completes, read `getProjectState()` and pass it to `applyProjectState()` for the task list and the scratchpad.
|
|
80
80
|
- `questions` is always empty: FlexHarness has no question requests.
|
|
81
|
+
- A cache-warm turn (see the FlexHarness readme, Keeping the Prompt Cache Warm) has no rows and leaves the status idle, the session card normal and `activeQueueId` unset, so a steer never joins it. Its usage still counts in `usage`: it is spent.
|
|
81
82
|
- Messages the session has not taken in yet (a queued prompt, a steer) carry no text in FlexHarness events. The host that sent them shows them as pending rows.
|
|
82
83
|
- An attachment row needs a name, a media type and a size. The public projection keeps no payload and states these only when the prompt did, so an attachment without all three, such as a remote URL of unknown size, has no row.
|
|
83
84
|
|