@modelprofile.com/flexharness 8.2.0 → 8.4.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.d.ts +11 -0
- package/dist_ts/classes.flexharness.js +91 -13
- package/dist_ts/errors.d.ts +4 -1
- package/dist_ts/errors.js +20 -1
- package/dist_ts/interfaces.d.ts +42 -1
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +4 -1
- package/dist_ts/plugins.js +3 -1
- package/dist_ts/utils.json.js +11 -2
- package/dist_ts/utils.validation.js +23 -15
- package/dist_ts_compaction/plugins.d.ts +5 -2
- package/dist_ts_compaction/plugins.js +5 -3
- package/dist_ts_compaction/smartagent.compaction.d.ts +8 -1
- package/dist_ts_compaction/smartagent.compaction.js +52 -10
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +99 -11
- package/ts/errors.ts +22 -1
- package/ts/interfaces.ts +45 -0
- package/ts/plugins.ts +9 -0
- package/ts/utils.json.ts +11 -1
- package/ts/utils.validation.ts +22 -16
- package/ts_compaction/plugins.ts +8 -2
- package/ts_compaction/smartagent.compaction.ts +56 -9
|
@@ -26,24 +26,66 @@ When constructing the summary, try to stick to this template:
|
|
|
26
26
|
*
|
|
27
27
|
* @param model The same model used by runAgent, or a cheaper small model
|
|
28
28
|
* @param messages The full message history that overflowed
|
|
29
|
-
* @param options Invocation options, including cancellation
|
|
29
|
+
* @param options Invocation options, including cancellation and usage reporting
|
|
30
30
|
* @returns A minimal ModelMessage[] containing the summary as context
|
|
31
31
|
*/
|
|
32
32
|
export async function compactMessages(model, messages, options = {}) {
|
|
33
|
-
const
|
|
33
|
+
const prompt = [
|
|
34
|
+
...messages,
|
|
35
|
+
{ role: 'user', content: COMPACTION_PROMPT },
|
|
36
|
+
];
|
|
37
|
+
const reportUsage = options.reportUsage;
|
|
38
|
+
if (!reportUsage) {
|
|
39
|
+
const result = await plugins.generateText({
|
|
40
|
+
model,
|
|
41
|
+
abortSignal: options.abortSignal,
|
|
42
|
+
allowSystemInMessages: true,
|
|
43
|
+
messages: prompt,
|
|
44
|
+
});
|
|
45
|
+
return summaryMessages(result.text);
|
|
46
|
+
}
|
|
47
|
+
const modelCalls = new plugins.AgentModelCallUsageRecorder(model, reportUsage);
|
|
48
|
+
// Each attempt, retries included, is one model call: it starts in the middleware, which also
|
|
49
|
+
// settles an attempt that throws; a call whose response arrives ends in onLanguageModelCallEnd.
|
|
50
|
+
const observedModel = plugins.wrapLanguageModel({
|
|
34
51
|
model,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
middleware: {
|
|
53
|
+
wrapGenerate: async ({ doGenerate, params }) => {
|
|
54
|
+
modelCalls.start();
|
|
55
|
+
try {
|
|
56
|
+
return await doGenerate();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
modelCalls.settle(params.abortSignal?.aborted ? 'aborted' : 'failed');
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
},
|
|
41
64
|
});
|
|
65
|
+
try {
|
|
66
|
+
const result = await plugins.generateText({
|
|
67
|
+
model: observedModel,
|
|
68
|
+
abortSignal: options.abortSignal,
|
|
69
|
+
allowSystemInMessages: true,
|
|
70
|
+
messages: prompt,
|
|
71
|
+
onLanguageModelCallEnd: ({ modelId, usage }) => {
|
|
72
|
+
modelCalls.end(modelId, usage);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
modelCalls.settle('missing');
|
|
76
|
+
return summaryMessages(result.text);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
modelCalls.settle(options.abortSignal?.aborted ? 'aborted' : 'failed');
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function summaryMessages(summary) {
|
|
42
84
|
return [
|
|
43
85
|
{
|
|
44
86
|
role: 'user',
|
|
45
|
-
content: `[Previous conversation summary]\n\n${
|
|
87
|
+
content: `[Previous conversation summary]\n\n${summary}\n\n[End of summary. Continue from here.]`,
|
|
46
88
|
},
|
|
47
89
|
];
|
|
48
90
|
}
|
|
49
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
91
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhZ2VudC5jb21wYWN0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfY29tcGFjdGlvbi9zbWFydGFnZW50LmNvbXBhY3Rpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFFeEMsTUFBTSxpQkFBaUIsR0FBRzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7SUFvQnRCLENBQUM7QUFhTDs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsZUFBZSxDQUNuQyxLQUFvQyxFQUNwQyxRQUFnQyxFQUNoQyxVQUFtQyxFQUFFO0lBRXJDLE1BQU0sTUFBTSxHQUEyQjtRQUNyQyxHQUFHLFFBQVE7UUFDWCxFQUFFLElBQUksRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLGlCQUFpQixFQUFFO0tBQzdDLENBQUM7SUFDRixNQUFNLFdBQVcsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDO0lBQ3hDLElBQUksQ0FBQyxXQUFXLEVBQUUsQ0FBQztRQUNqQixNQUFNLE1BQU0sR0FBRyxNQUFNLE9BQU8sQ0FBQyxZQUFZLENBQUM7WUFDeEMsS0FBSztZQUNMLFdBQVcsRUFBRSxPQUFPLENBQUMsV0FBVztZQUNoQyxxQkFBcUIsRUFBRSxJQUFJO1lBQzNCLFFBQVEsRUFBRSxNQUFNO1NBQ2pCLENBQUMsQ0FBQztRQUNILE9BQU8sZUFBZSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUN0QyxDQUFDO0lBQ0QsTUFBTSxVQUFVLEdBQUcsSUFBSSxPQUFPLENBQUMsMkJBQTJCLENBQUMsS0FBSyxFQUFFLFdBQVcsQ0FBQyxDQUFDO0lBQy9FLDZGQUE2RjtJQUM3RixnR0FBZ0c7SUFDaEcsTUFBTSxhQUFhLEdBQUcsT0FBTyxDQUFDLGlCQUFpQixDQUFDO1FBQzlDLEtBQUs7UUFDTCxVQUFVLEVBQUU7WUFDVixZQUFZLEVBQUUsS0FBSyxFQUFFLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxFQUFFLEVBQUU7Z0JBQzdDLFVBQVUsQ0FBQyxLQUFLLEVBQUUsQ0FBQztnQkFDbkIsSUFBSSxDQUFDO29CQUNILE9BQU8sTUFBTSxVQUFVLEVBQUUsQ0FBQztnQkFDNUIsQ0FBQztnQkFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO29CQUNmLFVBQVUsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLFdBQVcsRUFBRSxPQUFPLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUM7b0JBQ3RFLE1BQU0sS0FBSyxDQUFDO2dCQUNkLENBQUM7WUFDSCxDQUFDO1NBQ0Y7S0FDRixDQUFDLENBQUM7SUFDSCxJQUFJLENBQUM7UUFDSCxNQUFNLE1BQU0sR0FBRyxNQUFNLE9BQU8sQ0FBQyxZQUFZLENBQUM7WUFDeEMsS0FBSyxFQUFFLGFBQWE7WUFDcEIsV0FBVyxFQUFFLE9BQU8sQ0FBQyxXQUFXO1lBQ2hDLHFCQUFxQixFQUFFLElBQUk7WUFDM0IsUUFBUSxFQUFFLE1BQU07WUFDaEIsc0JBQXNCLEVBQUUsQ0FBQyxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsRUFBRSxFQUFFO2dCQUM3QyxVQUFVLENBQUMsR0FBRyxDQUFDLE9BQU8sRUFBRSxLQUFLLENBQUMsQ0FBQztZQUNqQyxDQUFDO1NBQ0YsQ0FBQyxDQUFDO1FBQ0gsVUFBVSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUM3QixPQUFPLGVBQWUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDdEMsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixVQUFVLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxXQUFXLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3ZFLE1BQU0sS0FBSyxDQUFDO0lBQ2QsQ0FBQztBQUNILENBQUM7QUFFRCxTQUFTLGVBQWUsQ0FBQyxPQUFlO0lBQ3RDLE9BQU87UUFDTDtZQUNFLElBQUksRUFBRSxNQUFNO1lBQ1osT0FBTyxFQUFFLHNDQUFzQyxPQUFPLDJDQUEyQztTQUNsRztLQUNGLENBQUM7QUFDSixDQUFDIn0=
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "Task Venture Capital GmbH",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "@modelprofile.com/flexharness",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.4.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"description": "Managed agent sessions with tools, MCP, chat, OCR, documents and browser control through focused subpath imports.",
|
|
8
8
|
"main": "./dist_ts/index.js",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@modelprofile.com/flexharness-agent": "8.
|
|
66
|
-
"@modelprofile.com/flexharness-models": "8.
|
|
65
|
+
"@modelprofile.com/flexharness-agent": "8.4.0",
|
|
66
|
+
"@modelprofile.com/flexharness-models": "8.4.0",
|
|
67
67
|
"@push.rocks/smartrequest": "^5.0.1",
|
|
68
68
|
"ai": "^7.0.79",
|
|
69
69
|
"zod": "^4.4.1"
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
FlexHarnessStoreCommitUncertainError,
|
|
17
17
|
FlexHarnessValidationError,
|
|
18
18
|
errorToInfo,
|
|
19
|
+
modelLimitErrorInfo,
|
|
19
20
|
} from './errors.js';
|
|
20
21
|
import {
|
|
21
22
|
FLEX_PROJECT_MANAGEMENT_LIMITS,
|
|
@@ -79,6 +80,7 @@ import type {
|
|
|
79
80
|
IFlexSchedulePromptOptions,
|
|
80
81
|
IFlexScopeSnapshot,
|
|
81
82
|
IFlexSession,
|
|
83
|
+
IFlexSessionActivity,
|
|
82
84
|
IFlexSessionGeneration,
|
|
83
85
|
IFlexSessionGenerationCohortEntry,
|
|
84
86
|
IFlexSessionReversionGroup,
|
|
@@ -97,6 +99,7 @@ import type {
|
|
|
97
99
|
IFlexUndoSessionResult,
|
|
98
100
|
IFlexUpdateSessionOptions,
|
|
99
101
|
IFlexUpdateProjectTaskInput,
|
|
102
|
+
IFlexUsage,
|
|
100
103
|
IJsonObject,
|
|
101
104
|
TFlexAgentToolCallUpdateEvent,
|
|
102
105
|
TFlexAgentToolCallFinishEvent,
|
|
@@ -335,6 +338,8 @@ interface IActiveRun {
|
|
|
335
338
|
callbackParts: TFlexMessagePart[];
|
|
336
339
|
partTextUtf8Bytes: Map<string, number>;
|
|
337
340
|
callbacksClosed: boolean;
|
|
341
|
+
/** The usage the provider reported for the run's model calls so far. */
|
|
342
|
+
usage: IFlexUsage;
|
|
338
343
|
reasoningPartIds: Map<string, string>;
|
|
339
344
|
toolPartIds: Map<string, string>;
|
|
340
345
|
trustedToolExecutionErrors: Map<string, Error>;
|
|
@@ -658,6 +663,14 @@ function combineErrors(errors: unknown[]): unknown {
|
|
|
658
663
|
return Object.freeze(combined);
|
|
659
664
|
}
|
|
660
665
|
|
|
666
|
+
/** The error and model limit a finished run leaves on its session activity. */
|
|
667
|
+
function terminalActivityError(message: IFlexMessage): Pick<IFlexSessionActivity, 'error' | 'limit'> {
|
|
668
|
+
return {
|
|
669
|
+
...(message.error ? { error: message.error } : {}),
|
|
670
|
+
...(message.limit ? { limit: message.limit } : {}),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
661
674
|
function isAbortError(error: unknown): boolean {
|
|
662
675
|
return error instanceof FlexHarnessAbortError
|
|
663
676
|
|| (error instanceof DOMException && error.name === 'AbortError')
|
|
@@ -3386,6 +3399,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3386
3399
|
callbackParts: [],
|
|
3387
3400
|
partTextUtf8Bytes: new Map(),
|
|
3388
3401
|
callbacksClosed: false,
|
|
3402
|
+
usage: {
|
|
3403
|
+
inputTokens: 0,
|
|
3404
|
+
outputTokens: 0,
|
|
3405
|
+
totalTokens: 0,
|
|
3406
|
+
cacheReadTokens: 0,
|
|
3407
|
+
cacheWriteTokens: 0,
|
|
3408
|
+
},
|
|
3389
3409
|
reasoningPartIds: new Map(),
|
|
3390
3410
|
toolPartIds: new Map(),
|
|
3391
3411
|
trustedToolExecutionErrors: new Map(),
|
|
@@ -3947,6 +3967,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3947
3967
|
): Promise<IFlexPromptResult> {
|
|
3948
3968
|
let generated = false;
|
|
3949
3969
|
try {
|
|
3970
|
+
// The transactional generation settles only after its model calls are reported, so
|
|
3971
|
+
// `run.usage` is complete once it settles, also for a cancelled run.
|
|
3950
3972
|
const rawResult = await generation;
|
|
3951
3973
|
generated = true;
|
|
3952
3974
|
if (run.callbackError) throw run.callbackError;
|
|
@@ -5676,9 +5698,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5676
5698
|
}
|
|
5677
5699
|
assistantMessage.usage = cloneSerializable(result.usage);
|
|
5678
5700
|
} else {
|
|
5701
|
+
assistantMessage.usage = cloneSerializable(run.usage);
|
|
5679
5702
|
const message = truncateUtf8(error?.message ?? externalErrorFallback.message, maxTransferMetadataBytes);
|
|
5680
|
-
|
|
5681
|
-
assistantMessage
|
|
5703
|
+
const limit = error instanceof FlexHarnessExternalError ? error.limit : undefined;
|
|
5704
|
+
for (const failedMessage of [userMessage, assistantMessage]) {
|
|
5705
|
+
failedMessage.error = message;
|
|
5706
|
+
if (limit) failedMessage.limit = cloneSerializable(limit);
|
|
5707
|
+
}
|
|
5682
5708
|
}
|
|
5683
5709
|
return {
|
|
5684
5710
|
runId: run.runId,
|
|
@@ -5692,7 +5718,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5692
5718
|
finishReason: result.finishReason,
|
|
5693
5719
|
steps: result.steps,
|
|
5694
5720
|
}
|
|
5695
|
-
: {}),
|
|
5721
|
+
: { usage: cloneSerializable(run.usage) }),
|
|
5696
5722
|
};
|
|
5697
5723
|
}
|
|
5698
5724
|
|
|
@@ -6766,7 +6792,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6766
6792
|
status: activityStatus,
|
|
6767
6793
|
startedAt: run.stored.session.activity.startedAt,
|
|
6768
6794
|
completedAt: timestamp,
|
|
6769
|
-
...(terminal.assistantMessage
|
|
6795
|
+
...terminalActivityError(terminal.assistantMessage),
|
|
6770
6796
|
};
|
|
6771
6797
|
run.stored.session.updatedAt = timestamp;
|
|
6772
6798
|
}, true);
|
|
@@ -7025,6 +7051,63 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7025
7051
|
}
|
|
7026
7052
|
}
|
|
7027
7053
|
|
|
7054
|
+
private onRetry(state: IStorageState, sessionId: string, retry: plugins.IAgentRetryEvent): void {
|
|
7055
|
+
const run = this.callbackRun(state, sessionId);
|
|
7056
|
+
if (!run || !this.reserveCallbackCapacity(run, 1, 0, 0)) return;
|
|
7057
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
7058
|
+
type: 'run.retrying',
|
|
7059
|
+
runId: run.runId,
|
|
7060
|
+
attempt: retry.attempt,
|
|
7061
|
+
maxAttempts: retry.maxAttempts,
|
|
7062
|
+
delayMs: retry.delayMs,
|
|
7063
|
+
reason: retry.reason,
|
|
7064
|
+
});
|
|
7065
|
+
}
|
|
7066
|
+
|
|
7067
|
+
/**
|
|
7068
|
+
* Emits the usage of every model call and adds reported usage to its run. A run executes a
|
|
7069
|
+
* transactional generation, and `AgentSession.generate()` settles a transactional generation only
|
|
7070
|
+
* after it has reported each of its model calls, also when it is aborted. `executeRun()` awaits
|
|
7071
|
+
* that settlement before it builds the terminal, so the run is still active for each of its
|
|
7072
|
+
* events: they all reach `run.usage` and precede its `run.finished`. The events bypass the
|
|
7073
|
+
* callback limits and a failed callback: their number is bounded by the run's model calls, and
|
|
7074
|
+
* the usage of a run that exhausted its callback budget or failed in a callback must not be lost.
|
|
7075
|
+
*/
|
|
7076
|
+
private onModelCallUsage(
|
|
7077
|
+
state: IStorageState,
|
|
7078
|
+
stored: IStoredSessionState,
|
|
7079
|
+
event: plugins.TAgentModelCallUsageEvent,
|
|
7080
|
+
): void {
|
|
7081
|
+
const { source, generationId, ...call } = event;
|
|
7082
|
+
const sessionId = stored.session.sessionId;
|
|
7083
|
+
const activeRun = state.activeRuns.get(sessionId);
|
|
7084
|
+
const run = generationId !== undefined && activeRun?.runId === generationId ? activeRun : undefined;
|
|
7085
|
+
let scopeId = run?.scopeId;
|
|
7086
|
+
if (scopeId === undefined) {
|
|
7087
|
+
const ambientContext = this.compactorInvocationContext.getStore();
|
|
7088
|
+
const invocationContext = ambientContext
|
|
7089
|
+
? this.deferredCompactorContexts.get(ambientContext) ?? ambientContext
|
|
7090
|
+
: undefined;
|
|
7091
|
+
if (
|
|
7092
|
+
!invocationContext
|
|
7093
|
+
|| invocationContext.storageKey !== state.storageKey
|
|
7094
|
+
|| invocationContext.sessionId !== sessionId
|
|
7095
|
+
) {
|
|
7096
|
+
throw new Error('Model call usage is missing its exact FlexHarness invocation context.');
|
|
7097
|
+
}
|
|
7098
|
+
scopeId = invocationContext.scopeId;
|
|
7099
|
+
}
|
|
7100
|
+
if (run && call.status === 'reported') {
|
|
7101
|
+
for (const key of Object.keys(run.usage) as (keyof IFlexUsage)[]) run.usage[key] += call.usage[key];
|
|
7102
|
+
}
|
|
7103
|
+
this.emitEvent(scopeId, stored.session, {
|
|
7104
|
+
type: 'model.usage',
|
|
7105
|
+
...(generationId === undefined ? {} : { runId: generationId }),
|
|
7106
|
+
source,
|
|
7107
|
+
...call,
|
|
7108
|
+
});
|
|
7109
|
+
}
|
|
7110
|
+
|
|
7028
7111
|
private callbackRun(state: IStorageState, sessionId: string): IActiveRun | undefined {
|
|
7029
7112
|
const run = state.activeRuns.get(sessionId);
|
|
7030
7113
|
return !run || run.callbacksClosed || run.callbackError ? undefined : run;
|
|
@@ -7777,7 +7860,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7777
7860
|
if ((typeof error === 'object' && error !== null) && this.trustedInternalErrors.has(error)) {
|
|
7778
7861
|
return error as Error;
|
|
7779
7862
|
}
|
|
7780
|
-
|
|
7863
|
+
const defaultInfo = modelLimitErrorInfo(error) ?? externalErrorFallback;
|
|
7864
|
+
let info = defaultInfo;
|
|
7781
7865
|
if (this.externalErrorProjector) {
|
|
7782
7866
|
try {
|
|
7783
7867
|
info = validateProjectedErrorInfo(this.externalErrorProjector(error, {
|
|
@@ -7787,7 +7871,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7787
7871
|
runId,
|
|
7788
7872
|
}));
|
|
7789
7873
|
} catch {
|
|
7790
|
-
info =
|
|
7874
|
+
info = defaultInfo;
|
|
7791
7875
|
}
|
|
7792
7876
|
}
|
|
7793
7877
|
return this.trustInternalError(new FlexHarnessExternalError(info));
|
|
@@ -8233,6 +8317,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8233
8317
|
onToolCallStart: (event) => this.onToolStart(state, sessionId, event),
|
|
8234
8318
|
onToolCallUpdate: (event) => this.onToolUpdate(state, sessionId, event),
|
|
8235
8319
|
onToolCallFinish: (event) => this.onToolFinish(state, sessionId, event),
|
|
8320
|
+
onRetry: (event) => this.onRetry(state, sessionId, event),
|
|
8321
|
+
onUsage: (event) => this.onModelCallUsage(state, stored, event),
|
|
8236
8322
|
};
|
|
8237
8323
|
stored.agentSession = await this.withCompactorContext(
|
|
8238
8324
|
compactorContext,
|
|
@@ -8405,9 +8491,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8405
8491
|
runId: latestTerminal.runId,
|
|
8406
8492
|
status: latestTerminal.status === 'completed' ? 'idle' : latestTerminal.status,
|
|
8407
8493
|
completedAt,
|
|
8408
|
-
...(latestTerminal.assistantMessage
|
|
8409
|
-
? { error: latestTerminal.assistantMessage.error }
|
|
8410
|
-
: {}),
|
|
8494
|
+
...terminalActivityError(latestTerminal.assistantMessage),
|
|
8411
8495
|
};
|
|
8412
8496
|
stored.session.updatedAt = completedAt;
|
|
8413
8497
|
} else if (['scheduled', 'running', 'waiting_permission'].includes(stored.session.status)) {
|
|
@@ -8490,7 +8574,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8490
8574
|
const message = status === 'cancelled' ? repairCancellationMessage : externalErrorFallback.message;
|
|
8491
8575
|
userMessage.error = message;
|
|
8492
8576
|
assistantMessage.error = message;
|
|
8493
|
-
|
|
8577
|
+
if (stage?.usage) {
|
|
8578
|
+
assistantMessage.usage = cloneSerializable(stage.usage);
|
|
8579
|
+
} else {
|
|
8580
|
+
delete assistantMessage.usage;
|
|
8581
|
+
}
|
|
8494
8582
|
}
|
|
8495
8583
|
return {
|
|
8496
8584
|
runId,
|
|
@@ -8498,7 +8586,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8498
8586
|
userMessage,
|
|
8499
8587
|
assistantMessage,
|
|
8500
8588
|
...(status === 'completed' && stage?.model ? { model: stage.model } : {}),
|
|
8501
|
-
...(
|
|
8589
|
+
...(stage?.usage ? { usage: stage.usage } : {}),
|
|
8502
8590
|
...(status === 'completed' && stage?.finishReason ? { finishReason: stage.finishReason } : {}),
|
|
8503
8591
|
...(status === 'completed' && stage?.steps !== undefined ? { steps: stage.steps } : {}),
|
|
8504
8592
|
};
|
package/ts/errors.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IFlexErrorInfo, TFlexModelLimit } from './interfaces.js';
|
|
3
|
+
|
|
4
|
+
const modelLimitErrorCodes: Record<TFlexModelLimit['kind'], string> = {
|
|
5
|
+
usage_limit: 'FLEX_USAGE_LIMIT',
|
|
6
|
+
rate_limit: 'FLEX_RATE_LIMIT',
|
|
7
|
+
};
|
|
2
8
|
|
|
3
9
|
export class FlexHarnessError extends Error {
|
|
4
10
|
public readonly code: string;
|
|
@@ -85,9 +91,12 @@ export class FlexHarnessCallbackOverflowError extends FlexHarnessError {
|
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
export class FlexHarnessExternalError extends FlexHarnessError {
|
|
94
|
+
public readonly limit: TFlexModelLimit | undefined;
|
|
95
|
+
|
|
88
96
|
constructor(info: IFlexErrorInfo) {
|
|
89
97
|
super(info.code ?? 'FLEX_EXTERNAL_ERROR', info.message);
|
|
90
98
|
this.name = info.name;
|
|
99
|
+
this.limit = info.limit;
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
102
|
|
|
@@ -164,6 +173,7 @@ export function errorToInfo(error: unknown): IFlexErrorInfo {
|
|
|
164
173
|
name: error.name,
|
|
165
174
|
message: error.message,
|
|
166
175
|
code: error.code,
|
|
176
|
+
...(error instanceof FlexHarnessExternalError && error.limit ? { limit: error.limit } : {}),
|
|
167
177
|
};
|
|
168
178
|
}
|
|
169
179
|
if (error instanceof Error) {
|
|
@@ -179,6 +189,17 @@ export function errorToInfo(error: unknown): IFlexErrorInfo {
|
|
|
179
189
|
};
|
|
180
190
|
}
|
|
181
191
|
|
|
192
|
+
/** The public projection of a model limit error, which names the limit and its reset time. */
|
|
193
|
+
export function modelLimitErrorInfo(error: unknown): IFlexErrorInfo | undefined {
|
|
194
|
+
if (!plugins.isModelLimitError(error)) return undefined;
|
|
195
|
+
return {
|
|
196
|
+
name: 'FlexHarnessExternalError',
|
|
197
|
+
message: error.message,
|
|
198
|
+
code: modelLimitErrorCodes[error.limit.kind],
|
|
199
|
+
limit: error.limit,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
182
203
|
export function errorMessage(error: unknown): string {
|
|
183
204
|
return error instanceof Error ? error.message : String(error);
|
|
184
205
|
}
|
package/ts/interfaces.ts
CHANGED
|
@@ -22,6 +22,15 @@ export type TFlexAgentToolCallUpdateEvent = Parameters<
|
|
|
22
22
|
export type TFlexAgentToolCallFinishEvent = Parameters<
|
|
23
23
|
NonNullable<TFlexAgentSessionOptions['onToolCallFinish']>
|
|
24
24
|
>[0];
|
|
25
|
+
/** A usage or rate limit a model provider reported; `resetsAt` is in epoch milliseconds. */
|
|
26
|
+
export type TFlexModelLimit = plugins.IModelLimitInfo;
|
|
27
|
+
export type TFlexRetryReason = plugins.TAgentRetryReason;
|
|
28
|
+
/**
|
|
29
|
+
* The usage outcome of one model call: `reported` with the provider's `usage` and
|
|
30
|
+
* `responseModelId`, or `unreported` with a `reason` (`aborted`, `failed` or `missing`).
|
|
31
|
+
* Every call carries `provider` and `requestedModelId`; key usage caps on those two.
|
|
32
|
+
*/
|
|
33
|
+
export type TFlexModelCallUsage = plugins.TAgentModelCallUsage;
|
|
25
34
|
|
|
26
35
|
export interface IFlexTextPromptPart {
|
|
27
36
|
type: 'text';
|
|
@@ -123,8 +132,15 @@ export interface IFlexMessage {
|
|
|
123
132
|
completedAt?: string;
|
|
124
133
|
parts: TFlexMessagePart[];
|
|
125
134
|
model?: IFlexModelIdentity;
|
|
135
|
+
/**
|
|
136
|
+
* On a terminal assistant message: the usage the provider reported for the run's model calls.
|
|
137
|
+
* For a failed or cancelled run it is a lower bound when a `model.usage` event of the run was
|
|
138
|
+
* `unreported`.
|
|
139
|
+
*/
|
|
126
140
|
usage?: IFlexUsage;
|
|
127
141
|
error?: string;
|
|
142
|
+
/** The model limit that failed this run, next to its error. */
|
|
143
|
+
limit?: TFlexModelLimit;
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
export interface IFlexSessionActivity {
|
|
@@ -133,6 +149,8 @@ export interface IFlexSessionActivity {
|
|
|
133
149
|
startedAt?: string;
|
|
134
150
|
completedAt?: string;
|
|
135
151
|
error?: string;
|
|
152
|
+
/** The model limit that failed the last run, next to its error. */
|
|
153
|
+
limit?: TFlexModelLimit;
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
export interface IFlexSession {
|
|
@@ -457,6 +475,7 @@ export interface IFlexTerminalProjection {
|
|
|
457
475
|
userMessage: IFlexMessage;
|
|
458
476
|
assistantMessage: IFlexMessage;
|
|
459
477
|
model?: IFlexModelIdentity;
|
|
478
|
+
/** The usage the provider reported for the run's model calls, whatever the status. */
|
|
460
479
|
usage?: IFlexUsage;
|
|
461
480
|
finishReason?: string;
|
|
462
481
|
steps?: number;
|
|
@@ -1227,6 +1246,28 @@ export interface IFlexRunFinishedEvent extends IFlexEventBase {
|
|
|
1227
1246
|
readonly error?: IFlexErrorInfo;
|
|
1228
1247
|
}
|
|
1229
1248
|
|
|
1249
|
+
/** The run waits `delayMs` before retrying a rate-limited or unavailable model call. */
|
|
1250
|
+
export interface IFlexRunRetryingEvent extends IFlexEventBase {
|
|
1251
|
+
readonly type: 'run.retrying';
|
|
1252
|
+
readonly runId: string;
|
|
1253
|
+
readonly attempt: number;
|
|
1254
|
+
readonly maxAttempts: number;
|
|
1255
|
+
readonly delayMs: number;
|
|
1256
|
+
readonly reason: TFlexRetryReason;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* The usage outcome of one model call of a session, emitted once per call whatever the run's
|
|
1261
|
+
* outcome. `source: 'generation'` is a model step of the run `runId`. `source: 'compaction'` is a
|
|
1262
|
+
* model call of a context compaction the context compactor reported; `runId` names the run whose
|
|
1263
|
+
* context overflow caused it and is absent for `compactSession()` and event retention.
|
|
1264
|
+
*/
|
|
1265
|
+
export type TFlexModelUsageEvent = IFlexEventBase & {
|
|
1266
|
+
readonly type: 'model.usage';
|
|
1267
|
+
readonly runId?: string;
|
|
1268
|
+
readonly source: 'generation' | 'compaction';
|
|
1269
|
+
} & Readonly<TFlexModelCallUsage>;
|
|
1270
|
+
|
|
1230
1271
|
export interface IFlexPromptQueueEvent extends IFlexEventBase {
|
|
1231
1272
|
readonly type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished';
|
|
1232
1273
|
readonly queueId: string;
|
|
@@ -1244,6 +1285,8 @@ export interface IFlexErrorInfo {
|
|
|
1244
1285
|
name: string;
|
|
1245
1286
|
message: string;
|
|
1246
1287
|
code?: string;
|
|
1288
|
+
/** Set when a model usage or rate limit caused the error. */
|
|
1289
|
+
limit?: TFlexModelLimit;
|
|
1247
1290
|
}
|
|
1248
1291
|
|
|
1249
1292
|
export type TFlexHarnessEvent =
|
|
@@ -1256,6 +1299,8 @@ export type TFlexHarnessEvent =
|
|
|
1256
1299
|
| IFlexPermissionRequestedEvent
|
|
1257
1300
|
| IFlexPermissionResolvedEvent
|
|
1258
1301
|
| IFlexRunFinishedEvent
|
|
1302
|
+
| IFlexRunRetryingEvent
|
|
1303
|
+
| TFlexModelUsageEvent
|
|
1259
1304
|
| IFlexPromptQueueEvent
|
|
1260
1305
|
| IFlexSessionHistoryChangedEvent;
|
|
1261
1306
|
|
package/ts/plugins.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type {
|
|
|
44
44
|
IAgentGenerateResult,
|
|
45
45
|
IAgentGenerationHandle,
|
|
46
46
|
IAgentGenerationLease,
|
|
47
|
+
IAgentRetryEvent,
|
|
47
48
|
IAgentRunResult,
|
|
48
49
|
IAgentSession,
|
|
49
50
|
IAgentSessionOptions,
|
|
@@ -62,6 +63,14 @@ export type {
|
|
|
62
63
|
TAgentContextCompactor,
|
|
63
64
|
TAgentEvent,
|
|
64
65
|
TAgentGenerationOutcome,
|
|
66
|
+
TAgentModelCallUsage,
|
|
67
|
+
TAgentModelCallUsageEvent,
|
|
68
|
+
TAgentRetryReason,
|
|
65
69
|
TAgentToolCallFinishEvent,
|
|
66
70
|
TAgentToolExecutionReconciliationOptions,
|
|
67
71
|
} from '@modelprofile.com/flexharness-agent';
|
|
72
|
+
|
|
73
|
+
import { isModelLimitError, isModelLimitInfo } from '@modelprofile.com/flexharness-models';
|
|
74
|
+
|
|
75
|
+
export { isModelLimitError, isModelLimitInfo };
|
|
76
|
+
export type { IModelLimitInfo } from '@modelprofile.com/flexharness-models';
|
package/ts/utils.json.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
1
2
|
import { FlexHarnessStoreFormatError, FlexHarnessValidationError } from './errors.js';
|
|
2
3
|
import {
|
|
3
4
|
FLEX_REVERSION_MAX_AFFECTED_WORKSPACES,
|
|
@@ -406,6 +407,12 @@ function requireOptionalString(value: unknown, path: string): string | undefined
|
|
|
406
407
|
return value === undefined ? undefined : requireString(value, path);
|
|
407
408
|
}
|
|
408
409
|
|
|
410
|
+
function requireOptionalModelLimit(value: unknown, path: string): void {
|
|
411
|
+
if (value !== undefined && !plugins.isModelLimitInfo(value)) {
|
|
412
|
+
throw new FlexHarnessStoreFormatError(`${path} is invalid.`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
409
416
|
function requireNonNegativeNumber(value: unknown, path: string): void {
|
|
410
417
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
411
418
|
throw new FlexHarnessStoreFormatError(`${path} must be a non-negative finite number.`);
|
|
@@ -501,7 +508,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
501
508
|
throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
|
|
502
509
|
}
|
|
503
510
|
const activity = requireRecord(session.activity, `${path}.activity`);
|
|
504
|
-
requireOnlyKeys(activity, ['runId', 'status', 'startedAt', 'completedAt', 'error'], `${path}.activity`);
|
|
511
|
+
requireOnlyKeys(activity, ['runId', 'status', 'startedAt', 'completedAt', 'error', 'limit'], `${path}.activity`);
|
|
505
512
|
if (!activityStatuses.includes(activity.status as (typeof activityStatuses)[number])) {
|
|
506
513
|
throw new FlexHarnessStoreFormatError(`${path}.activity.status is invalid.`);
|
|
507
514
|
}
|
|
@@ -509,6 +516,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
509
516
|
requireOptionalString(activity.startedAt, `${path}.activity.startedAt`);
|
|
510
517
|
requireOptionalString(activity.completedAt, `${path}.activity.completedAt`);
|
|
511
518
|
requireOptionalString(activity.error, `${path}.activity.error`);
|
|
519
|
+
requireOptionalModelLimit(activity.limit, `${path}.activity.limit`);
|
|
512
520
|
const relationship = [
|
|
513
521
|
session.parentSessionId,
|
|
514
522
|
session.parentRunId,
|
|
@@ -667,6 +675,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
667
675
|
'model',
|
|
668
676
|
'usage',
|
|
669
677
|
'error',
|
|
678
|
+
'limit',
|
|
670
679
|
],
|
|
671
680
|
path,
|
|
672
681
|
);
|
|
@@ -682,6 +691,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
682
691
|
requireString(message.createdAt, `${path}.createdAt`);
|
|
683
692
|
requireOptionalString(message.completedAt, `${path}.completedAt`);
|
|
684
693
|
requireOptionalString(message.error, `${path}.error`);
|
|
694
|
+
requireOptionalModelLimit(message.limit, `${path}.limit`);
|
|
685
695
|
if (message.model !== undefined) validateModel(message.model, `${path}.model`);
|
|
686
696
|
if (message.usage !== undefined) validateUsage(message.usage, `${path}.usage`);
|
|
687
697
|
if (!Array.isArray(message.parts)) {
|
package/ts/utils.validation.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
1
2
|
import { FlexHarnessValidationError } from './errors.js';
|
|
2
3
|
import {
|
|
3
4
|
FLEX_PROJECT_MANAGEMENT_LIMITS,
|
|
@@ -162,30 +163,30 @@ export function validateDeleteSessionGenerationCohortInput(
|
|
|
162
163
|
return { root, authorizedBySessionId };
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
|
|
166
|
+
/** A plain object whose own properties are all string-keyed data properties, so reading them runs no code. */
|
|
167
|
+
function isPlainDataObject(value: unknown): value is Record<string, unknown> {
|
|
166
168
|
if (
|
|
167
169
|
!value
|
|
168
170
|
|| typeof value !== 'object'
|
|
169
171
|
|| Array.isArray(value)
|
|
170
172
|
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
171
|
-
)
|
|
173
|
+
) return false;
|
|
174
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
175
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
176
|
+
return typeof key === 'string' && descriptor !== undefined && 'value' in descriptor;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function validateProjectedErrorInfo(value: unknown): IFlexErrorInfo {
|
|
181
|
+
if (!isPlainDataObject(value)) {
|
|
172
182
|
throw new FlexHarnessValidationError('External error projection is invalid.');
|
|
173
183
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
const name = descriptors.name?.value;
|
|
179
|
-
const message = descriptors.message?.value;
|
|
180
|
-
const code = descriptors.code?.value;
|
|
184
|
+
const unsupportedKey = Object.getOwnPropertyNames(value)
|
|
185
|
+
.find((key) => !['name', 'message', 'code', 'limit'].includes(key));
|
|
186
|
+
const { name, message, code, limit } = value;
|
|
181
187
|
if (
|
|
182
188
|
unsupportedKey
|
|
183
|
-
||
|
|
184
|
-
|| descriptors.name?.set !== undefined
|
|
185
|
-
|| descriptors.message?.get !== undefined
|
|
186
|
-
|| descriptors.message?.set !== undefined
|
|
187
|
-
|| descriptors.code?.get !== undefined
|
|
188
|
-
|| descriptors.code?.set !== undefined
|
|
189
|
+
|| (limit !== undefined && !(isPlainDataObject(limit) && plugins.isModelLimitInfo(limit)))
|
|
189
190
|
|| typeof name !== 'string'
|
|
190
191
|
|| !name
|
|
191
192
|
|| Buffer.byteLength(name, 'utf8') > maxProjectedErrorNameBytes
|
|
@@ -199,7 +200,12 @@ export function validateProjectedErrorInfo(value: unknown): IFlexErrorInfo {
|
|
|
199
200
|
) {
|
|
200
201
|
throw new FlexHarnessValidationError('External error projection is invalid.');
|
|
201
202
|
}
|
|
202
|
-
return Object.freeze({
|
|
203
|
+
return Object.freeze({
|
|
204
|
+
name,
|
|
205
|
+
message,
|
|
206
|
+
...(code === undefined ? {} : { code }),
|
|
207
|
+
...(limit === undefined ? {} : { limit: Object.freeze({ ...limit }) }),
|
|
208
|
+
});
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
export function validateUpdateSessionOptions(options: IFlexUpdateSessionOptions): void {
|
package/ts_compaction/plugins.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { generateText } from 'ai';
|
|
1
|
+
import { generateText, wrapLanguageModel } from 'ai';
|
|
2
2
|
|
|
3
|
-
export { generateText };
|
|
3
|
+
export { generateText, wrapLanguageModel };
|
|
4
4
|
|
|
5
5
|
export type { ModelMessage } from 'ai';
|
|
6
6
|
export type { TSmartAiLanguageModel } from '@modelprofile.com/flexharness-models';
|
|
7
|
+
|
|
8
|
+
import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
|
|
9
|
+
|
|
10
|
+
export { AgentModelCallUsageRecorder };
|
|
11
|
+
|
|
12
|
+
export type { TAgentModelCallUsageReporter } from '@modelprofile.com/flexharness-agent';
|