@modelprofile.com/flexharness 8.3.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.
@@ -17,6 +17,12 @@ export type TFlexAgentToolCallFinishEvent = Parameters<NonNullable<TFlexAgentSes
17
17
  /** A usage or rate limit a model provider reported; `resetsAt` is in epoch milliseconds. */
18
18
  export type TFlexModelLimit = plugins.IModelLimitInfo;
19
19
  export type TFlexRetryReason = plugins.TAgentRetryReason;
20
+ /**
21
+ * The usage outcome of one model call: `reported` with the provider's `usage` and
22
+ * `responseModelId`, or `unreported` with a `reason` (`aborted`, `failed` or `missing`).
23
+ * Every call carries `provider` and `requestedModelId`; key usage caps on those two.
24
+ */
25
+ export type TFlexModelCallUsage = plugins.TAgentModelCallUsage;
20
26
  export interface IFlexTextPromptPart {
21
27
  type: 'text';
22
28
  text: string;
@@ -95,6 +101,11 @@ export interface IFlexMessage {
95
101
  completedAt?: string;
96
102
  parts: TFlexMessagePart[];
97
103
  model?: IFlexModelIdentity;
104
+ /**
105
+ * On a terminal assistant message: the usage the provider reported for the run's model calls.
106
+ * For a failed or cancelled run it is a lower bound when a `model.usage` event of the run was
107
+ * `unreported`.
108
+ */
98
109
  usage?: IFlexUsage;
99
110
  error?: string;
100
111
  /** The model limit that failed this run, next to its error. */
@@ -356,6 +367,7 @@ export interface IFlexTerminalProjection {
356
367
  userMessage: IFlexMessage;
357
368
  assistantMessage: IFlexMessage;
358
369
  model?: IFlexModelIdentity;
370
+ /** The usage the provider reported for the run's model calls, whatever the status. */
359
371
  usage?: IFlexUsage;
360
372
  finishReason?: string;
361
373
  steps?: number;
@@ -933,6 +945,17 @@ export interface IFlexRunRetryingEvent extends IFlexEventBase {
933
945
  readonly delayMs: number;
934
946
  readonly reason: TFlexRetryReason;
935
947
  }
948
+ /**
949
+ * The usage outcome of one model call of a session, emitted once per call whatever the run's
950
+ * outcome. `source: 'generation'` is a model step of the run `runId`. `source: 'compaction'` is a
951
+ * model call of a context compaction the context compactor reported; `runId` names the run whose
952
+ * context overflow caused it and is absent for `compactSession()` and event retention.
953
+ */
954
+ export type TFlexModelUsageEvent = IFlexEventBase & {
955
+ readonly type: 'model.usage';
956
+ readonly runId?: string;
957
+ readonly source: 'generation' | 'compaction';
958
+ } & Readonly<TFlexModelCallUsage>;
936
959
  export interface IFlexPromptQueueEvent extends IFlexEventBase {
937
960
  readonly type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished';
938
961
  readonly queueId: string;
@@ -951,6 +974,6 @@ export interface IFlexErrorInfo {
951
974
  /** Set when a model usage or rate limit caused the error. */
952
975
  limit?: TFlexModelLimit;
953
976
  }
954
- export type TFlexHarnessEvent = IFlexSessionCreatedEvent | IFlexSessionUpdatedEvent | IFlexSessionDeletedEvent | IFlexRunStartedEvent | IFlexMessageChangedEvent | TFlexPartEvent | IFlexPermissionRequestedEvent | IFlexPermissionResolvedEvent | IFlexRunFinishedEvent | IFlexRunRetryingEvent | IFlexPromptQueueEvent | IFlexSessionHistoryChangedEvent;
977
+ export type TFlexHarnessEvent = IFlexSessionCreatedEvent | IFlexSessionUpdatedEvent | IFlexSessionDeletedEvent | IFlexRunStartedEvent | IFlexMessageChangedEvent | TFlexPartEvent | IFlexPermissionRequestedEvent | IFlexPermissionResolvedEvent | IFlexRunFinishedEvent | IFlexRunRetryingEvent | TFlexModelUsageEvent | IFlexPromptQueueEvent | IFlexSessionHistoryChangedEvent;
955
978
  export type TFlexHarnessEventListener = (event: TFlexHarnessEvent) => void;
956
979
  export {};
@@ -27,4 +27,4 @@ export const FLEX_REVERSION_MAX_AFFECTED_WORKSPACES = 64;
27
27
  export const FLEX_REVERSION_WORKSPACE_ID_MAX_BYTES = 512;
28
28
  export const FLEX_REVERSION_WORKSPACE_LABEL_MAX_BYTES = 2048;
29
29
  export const FLEX_REVERSION_REASON_CODE_MAX_BYTES = 128;
30
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2ludGVyZmFjZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBdWtCQSxNQUFNLENBQUMsTUFBTSxzQ0FBc0MsR0FBRyxDQUFVLENBQUM7QUFDakUsTUFBTSxDQUFDLE1BQU0sb0NBQW9DLEdBQUcsR0FBRyxDQUFDO0FBQ3hELE1BQU0sQ0FBQyxNQUFNLDBDQUEwQyxHQUFHLElBQUksQ0FBQztBQStCL0QsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUMxRCxZQUFZLEVBQUUsQ0FBQyxHQUFHLElBQUk7SUFDdEIsa0JBQWtCLEVBQUUsR0FBRyxHQUFHLElBQUk7SUFDOUIsbUJBQW1CLEVBQUUsQ0FBQyxHQUFHLElBQUk7SUFDN0IsY0FBYyxFQUFFLEdBQUc7SUFDbkIsUUFBUSxFQUFFLEdBQUc7SUFDYixhQUFhLEVBQUUsSUFBSTtJQUNuQixnQkFBZ0IsRUFBRSxJQUFJLEdBQUcsSUFBSTtDQUM5QixDQUFDLENBQUM7QUErTEgsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN6RCxpQkFBaUIsRUFBRSxHQUFHO0lBQ3RCLFdBQVcsRUFBRSxHQUFHO0lBQ2hCLGlCQUFpQixFQUFFLElBQUk7SUFDdkIsMkJBQTJCLEVBQUUsSUFBSTtDQUNsQyxDQUFDLENBQUM7QUFFSCxNQUFNLENBQUMsTUFBTSw2QkFBNkIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3pELGlCQUFpQixFQUFFLElBQUk7SUFDdkIsV0FBVyxFQUFFLElBQUk7SUFDakIsaUJBQWlCLEVBQUUsTUFBTTtJQUN6QiwyQkFBMkIsRUFBRSxNQUFNO0NBQ3BDLENBQUMsQ0FBQztBQUVILE1BQU0sQ0FBQyxNQUFNLGtDQUFrQyxHQUFHLEdBQUcsR0FBRyxJQUFJLENBQUM7QUFDN0QsTUFBTSxDQUFDLE1BQU0sc0NBQXNDLEdBQUcsRUFBRSxDQUFDO0FBQ3pELE1BQU0sQ0FBQyxNQUFNLHFDQUFxQyxHQUFHLEdBQUcsQ0FBQztBQUN6RCxNQUFNLENBQUMsTUFBTSx3Q0FBd0MsR0FBRyxJQUFJLENBQUM7QUFDN0QsTUFBTSxDQUFDLE1BQU0sb0NBQW9DLEdBQUcsR0FBRyxDQUFDIn0=
30
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2ludGVyZmFjZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBbWxCQSxNQUFNLENBQUMsTUFBTSxzQ0FBc0MsR0FBRyxDQUFVLENBQUM7QUFDakUsTUFBTSxDQUFDLE1BQU0sb0NBQW9DLEdBQUcsR0FBRyxDQUFDO0FBQ3hELE1BQU0sQ0FBQyxNQUFNLDBDQUEwQyxHQUFHLElBQUksQ0FBQztBQStCL0QsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUMxRCxZQUFZLEVBQUUsQ0FBQyxHQUFHLElBQUk7SUFDdEIsa0JBQWtCLEVBQUUsR0FBRyxHQUFHLElBQUk7SUFDOUIsbUJBQW1CLEVBQUUsQ0FBQyxHQUFHLElBQUk7SUFDN0IsY0FBYyxFQUFFLEdBQUc7SUFDbkIsUUFBUSxFQUFFLEdBQUc7SUFDYixhQUFhLEVBQUUsSUFBSTtJQUNuQixnQkFBZ0IsRUFBRSxJQUFJLEdBQUcsSUFBSTtDQUM5QixDQUFDLENBQUM7QUErTEgsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN6RCxpQkFBaUIsRUFBRSxHQUFHO0lBQ3RCLFdBQVcsRUFBRSxHQUFHO0lBQ2hCLGlCQUFpQixFQUFFLElBQUk7SUFDdkIsMkJBQTJCLEVBQUUsSUFBSTtDQUNsQyxDQUFDLENBQUM7QUFFSCxNQUFNLENBQUMsTUFBTSw2QkFBNkIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3pELGlCQUFpQixFQUFFLElBQUk7SUFDdkIsV0FBVyxFQUFFLElBQUk7SUFDakIsaUJBQWlCLEVBQUUsTUFBTTtJQUN6QiwyQkFBMkIsRUFBRSxNQUFNO0NBQ3BDLENBQUMsQ0FBQztBQUVILE1BQU0sQ0FBQyxNQUFNLGtDQUFrQyxHQUFHLEdBQUcsR0FBRyxJQUFJLENBQUM7QUFDN0QsTUFBTSxDQUFDLE1BQU0sc0NBQXNDLEdBQUcsRUFBRSxDQUFDO0FBQ3pELE1BQU0sQ0FBQyxNQUFNLHFDQUFxQyxHQUFHLEdBQUcsQ0FBQztBQUN6RCxNQUFNLENBQUMsTUFBTSx3Q0FBd0MsR0FBRyxJQUFJLENBQUM7QUFDN0QsTUFBTSxDQUFDLE1BQU0sb0NBQW9DLEdBQUcsR0FBRyxDQUFDIn0=
@@ -5,7 +5,7 @@ import * as path from 'node:path';
5
5
  export { AsyncLocalStorage, crypto, fs, path };
6
6
  import { AgentEventStoreConflictError, AgentSession, buildModelMessages, ToolJobStoreConflictError, createAgentEvent, filterModelVisibleAgentEvents, getAgentGenerationTransactions, modelMessagesToAgentEvents, tool, validateAgentEventArchiveV2, validateAgentEventSnapshotV2, z } from '@modelprofile.com/flexharness-agent';
7
7
  export { AgentEventStoreConflictError, AgentSession, buildModelMessages, ToolJobStoreConflictError, createAgentEvent, filterModelVisibleAgentEvents, getAgentGenerationTransactions, modelMessagesToAgentEvents, tool, validateAgentEventArchiveV2, validateAgentEventSnapshotV2, z, };
8
- export type { IAgentEventArchiveV2, IAgentContextCompactionOptions, IAgentEventSnapshotV2, IAgentEventStoreV2, IAgentGenerateResult, IAgentGenerationHandle, IAgentGenerationLease, IAgentRetryEvent, IAgentRunResult, IAgentSession, IAgentSessionOptions, IAgentToolCallUpdateEvent, IAgentRuntimeEventPayload, IGenerationBegunEvent, IGenerationExecutionCompletedEvent, IGenerationExecutionStartedEvent, IGenerationOutcomeEvent, IToolExecutionContext, IToolExecutionIntentEvent, IToolJobSnapshot, IToolJobState, IToolJobStore, TAgentContextBuilder, TAgentContextCompactor, TAgentEvent, TAgentGenerationOutcome, TAgentRetryReason, TAgentToolCallFinishEvent, TAgentToolExecutionReconciliationOptions, } from '@modelprofile.com/flexharness-agent';
8
+ export type { IAgentEventArchiveV2, IAgentContextCompactionOptions, IAgentEventSnapshotV2, IAgentEventStoreV2, IAgentGenerateResult, IAgentGenerationHandle, IAgentGenerationLease, IAgentRetryEvent, IAgentRunResult, IAgentSession, IAgentSessionOptions, IAgentToolCallUpdateEvent, IAgentRuntimeEventPayload, IGenerationBegunEvent, IGenerationExecutionCompletedEvent, IGenerationExecutionStartedEvent, IGenerationOutcomeEvent, IToolExecutionContext, IToolExecutionIntentEvent, IToolJobSnapshot, IToolJobState, IToolJobStore, TAgentContextBuilder, TAgentContextCompactor, TAgentEvent, TAgentGenerationOutcome, TAgentModelCallUsage, TAgentModelCallUsageEvent, TAgentRetryReason, TAgentToolCallFinishEvent, TAgentToolExecutionReconciliationOptions, } from '@modelprofile.com/flexharness-agent';
9
9
  import { isModelLimitError, isModelLimitInfo } from '@modelprofile.com/flexharness-models';
10
10
  export { isModelLimitError, isModelLimitInfo };
11
11
  export type { IModelLimitInfo } from '@modelprofile.com/flexharness-models';
@@ -9,4 +9,4 @@ import { AgentEventStoreConflictError, AgentSession, buildModelMessages, ToolJob
9
9
  export { AgentEventStoreConflictError, AgentSession, buildModelMessages, ToolJobStoreConflictError, createAgentEvent, filterModelVisibleAgentEvents, getAgentGenerationTransactions, modelMessagesToAgentEvents, tool, validateAgentEventArchiveV2, validateAgentEventSnapshotV2, z, };
10
10
  import { isModelLimitError, isModelLimitInfo } from '@modelprofile.com/flexharness-models';
11
11
  export { isModelLimitError, isModelLimitInfo };
12
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ3JELE9BQU8sS0FBSyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDdkMsT0FBTyxLQUFLLElBQUksTUFBTSxXQUFXLENBQUM7QUFFbEMsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLENBQUM7QUFFL0Msb0JBQW9CO0FBQ3BCLE9BQU8sRUFDTCw0QkFBNEIsRUFDNUIsWUFBWSxFQUNaLGtCQUFrQixFQUNsQix5QkFBeUIsRUFDekIsZ0JBQWdCLEVBQ2hCLDZCQUE2QixFQUM3Qiw4QkFBOEIsRUFDOUIsMEJBQTBCLEVBQzFCLElBQUksRUFDSiwyQkFBMkIsRUFDM0IsNEJBQTRCLEVBQzVCLENBQUMsR0FDRixNQUFNLHFDQUFxQyxDQUFDO0FBRTdDLE9BQU8sRUFDTCw0QkFBNEIsRUFDNUIsWUFBWSxFQUNaLGtCQUFrQixFQUNsQix5QkFBeUIsRUFDekIsZ0JBQWdCLEVBQ2hCLDZCQUE2QixFQUM3Qiw4QkFBOEIsRUFDOUIsMEJBQTBCLEVBQzFCLElBQUksRUFDSiwyQkFBMkIsRUFDM0IsNEJBQTRCLEVBQzVCLENBQUMsR0FDRixDQUFDO0FBaUNGLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLHNDQUFzQyxDQUFDO0FBRTNGLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxnQkFBZ0IsRUFBRSxDQUFDIn0=
12
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ3JELE9BQU8sS0FBSyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDdkMsT0FBTyxLQUFLLElBQUksTUFBTSxXQUFXLENBQUM7QUFFbEMsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLENBQUM7QUFFL0Msb0JBQW9CO0FBQ3BCLE9BQU8sRUFDTCw0QkFBNEIsRUFDNUIsWUFBWSxFQUNaLGtCQUFrQixFQUNsQix5QkFBeUIsRUFDekIsZ0JBQWdCLEVBQ2hCLDZCQUE2QixFQUM3Qiw4QkFBOEIsRUFDOUIsMEJBQTBCLEVBQzFCLElBQUksRUFDSiwyQkFBMkIsRUFDM0IsNEJBQTRCLEVBQzVCLENBQUMsR0FDRixNQUFNLHFDQUFxQyxDQUFDO0FBRTdDLE9BQU8sRUFDTCw0QkFBNEIsRUFDNUIsWUFBWSxFQUNaLGtCQUFrQixFQUNsQix5QkFBeUIsRUFDekIsZ0JBQWdCLEVBQ2hCLDZCQUE2QixFQUM3Qiw4QkFBOEIsRUFDOUIsMEJBQTBCLEVBQzFCLElBQUksRUFDSiwyQkFBMkIsRUFDM0IsNEJBQTRCLEVBQzVCLENBQUMsR0FDRixDQUFDO0FBbUNGLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLHNDQUFzQyxDQUFDO0FBRTNGLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxnQkFBZ0IsRUFBRSxDQUFDIn0=
@@ -1,4 +1,7 @@
1
- import { generateText } from 'ai';
2
- export { generateText };
1
+ import { generateText, wrapLanguageModel } from 'ai';
2
+ export { generateText, wrapLanguageModel };
3
3
  export type { ModelMessage } from 'ai';
4
4
  export type { TSmartAiLanguageModel } from '@modelprofile.com/flexharness-models';
5
+ import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
6
+ export { AgentModelCallUsageRecorder };
7
+ export type { TAgentModelCallUsageReporter } from '@modelprofile.com/flexharness-agent';
@@ -1,3 +1,5 @@
1
- import { generateText } from 'ai';
2
- export { generateText };
3
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX2NvbXBhY3Rpb24vcGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sSUFBSSxDQUFDO0FBRWxDLE9BQU8sRUFBRSxZQUFZLEVBQUUsQ0FBQyJ9
1
+ import { generateText, wrapLanguageModel } from 'ai';
2
+ export { generateText, wrapLanguageModel };
3
+ import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
4
+ export { AgentModelCallUsageRecorder };
5
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX2NvbXBhY3Rpb24vcGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsWUFBWSxFQUFFLGlCQUFpQixFQUFFLE1BQU0sSUFBSSxDQUFDO0FBRXJELE9BQU8sRUFBRSxZQUFZLEVBQUUsaUJBQWlCLEVBQUUsQ0FBQztBQUszQyxPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxxQ0FBcUMsQ0FBQztBQUVsRixPQUFPLEVBQUUsMkJBQTJCLEVBQUUsQ0FBQyJ9
@@ -1,6 +1,13 @@
1
1
  import * as plugins from './plugins.js';
2
2
  export interface ICompactMessagesOptions {
3
3
  abortSignal?: AbortSignal;
4
+ /**
5
+ * Receives the usage of every model call the compaction makes, once per call: `reported` with the
6
+ * provider's usage, or `unreported` for a call that failed or was aborted, retried attempts
7
+ * included. A context compactor passes its own options through, so the calls reach the session's
8
+ * `onUsage`. Must not throw.
9
+ */
10
+ reportUsage?: plugins.TAgentModelCallUsageReporter;
4
11
  }
5
12
  /**
6
13
  * Compacts a message history into a summary.
@@ -8,7 +15,7 @@ export interface ICompactMessagesOptions {
8
15
  *
9
16
  * @param model The same model used by runAgent, or a cheaper small model
10
17
  * @param messages The full message history that overflowed
11
- * @param options Invocation options, including cancellation
18
+ * @param options Invocation options, including cancellation and usage reporting
12
19
  * @returns A minimal ModelMessage[] containing the summary as context
13
20
  */
14
21
  export declare function compactMessages(model: plugins.TSmartAiLanguageModel, messages: plugins.ModelMessage[], options?: ICompactMessagesOptions): Promise<plugins.ModelMessage[]>;
@@ -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 result = await plugins.generateText({
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
- abortSignal: options.abortSignal,
36
- allowSystemInMessages: true,
37
- messages: [
38
- ...messages,
39
- { role: 'user', content: COMPACTION_PROMPT },
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${result.text}\n\n[End of summary. Continue from here.]`,
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhZ2VudC5jb21wYWN0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfY29tcGFjdGlvbi9zbWFydGFnZW50LmNvbXBhY3Rpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFFeEMsTUFBTSxpQkFBaUIsR0FBRzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7SUFvQnRCLENBQUM7QUFNTDs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsZUFBZSxDQUNuQyxLQUFvQyxFQUNwQyxRQUFnQyxFQUNoQyxVQUFtQyxFQUFFO0lBRXJDLE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLFlBQVksQ0FBQztRQUN4QyxLQUFLO1FBQ0wsV0FBVyxFQUFFLE9BQU8sQ0FBQyxXQUFXO1FBQ2hDLHFCQUFxQixFQUFFLElBQUk7UUFDM0IsUUFBUSxFQUFFO1lBQ1IsR0FBRyxRQUFRO1lBQ1gsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxpQkFBaUIsRUFBRTtTQUM3QztLQUNGLENBQUMsQ0FBQztJQUVILE9BQU87UUFDTDtZQUNFLElBQUksRUFBRSxNQUFNO1lBQ1osT0FBTyxFQUFFLHNDQUFzQyxNQUFNLENBQUMsSUFBSSwyQ0FBMkM7U0FDdEc7S0FDRixDQUFDO0FBQ0osQ0FBQyJ9
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.3.0",
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.3.0",
66
- "@modelprofile.com/flexharness-models": "8.3.0",
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"
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '8.3.0',
6
+ version: '8.4.0',
7
7
  description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
8
8
  }
@@ -99,6 +99,7 @@ import type {
99
99
  IFlexUndoSessionResult,
100
100
  IFlexUpdateSessionOptions,
101
101
  IFlexUpdateProjectTaskInput,
102
+ IFlexUsage,
102
103
  IJsonObject,
103
104
  TFlexAgentToolCallUpdateEvent,
104
105
  TFlexAgentToolCallFinishEvent,
@@ -337,6 +338,8 @@ interface IActiveRun {
337
338
  callbackParts: TFlexMessagePart[];
338
339
  partTextUtf8Bytes: Map<string, number>;
339
340
  callbacksClosed: boolean;
341
+ /** The usage the provider reported for the run's model calls so far. */
342
+ usage: IFlexUsage;
340
343
  reasoningPartIds: Map<string, string>;
341
344
  toolPartIds: Map<string, string>;
342
345
  trustedToolExecutionErrors: Map<string, Error>;
@@ -3396,6 +3399,13 @@ export class FlexHarness<TScope = unknown> {
3396
3399
  callbackParts: [],
3397
3400
  partTextUtf8Bytes: new Map(),
3398
3401
  callbacksClosed: false,
3402
+ usage: {
3403
+ inputTokens: 0,
3404
+ outputTokens: 0,
3405
+ totalTokens: 0,
3406
+ cacheReadTokens: 0,
3407
+ cacheWriteTokens: 0,
3408
+ },
3399
3409
  reasoningPartIds: new Map(),
3400
3410
  toolPartIds: new Map(),
3401
3411
  trustedToolExecutionErrors: new Map(),
@@ -3957,6 +3967,8 @@ export class FlexHarness<TScope = unknown> {
3957
3967
  ): Promise<IFlexPromptResult> {
3958
3968
  let generated = false;
3959
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.
3960
3972
  const rawResult = await generation;
3961
3973
  generated = true;
3962
3974
  if (run.callbackError) throw run.callbackError;
@@ -5686,6 +5698,7 @@ export class FlexHarness<TScope = unknown> {
5686
5698
  }
5687
5699
  assistantMessage.usage = cloneSerializable(result.usage);
5688
5700
  } else {
5701
+ assistantMessage.usage = cloneSerializable(run.usage);
5689
5702
  const message = truncateUtf8(error?.message ?? externalErrorFallback.message, maxTransferMetadataBytes);
5690
5703
  const limit = error instanceof FlexHarnessExternalError ? error.limit : undefined;
5691
5704
  for (const failedMessage of [userMessage, assistantMessage]) {
@@ -5705,7 +5718,7 @@ export class FlexHarness<TScope = unknown> {
5705
5718
  finishReason: result.finishReason,
5706
5719
  steps: result.steps,
5707
5720
  }
5708
- : {}),
5721
+ : { usage: cloneSerializable(run.usage) }),
5709
5722
  };
5710
5723
  }
5711
5724
 
@@ -7051,6 +7064,50 @@ export class FlexHarness<TScope = unknown> {
7051
7064
  });
7052
7065
  }
7053
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
+
7054
7111
  private callbackRun(state: IStorageState, sessionId: string): IActiveRun | undefined {
7055
7112
  const run = state.activeRuns.get(sessionId);
7056
7113
  return !run || run.callbacksClosed || run.callbackError ? undefined : run;
@@ -8261,6 +8318,7 @@ export class FlexHarness<TScope = unknown> {
8261
8318
  onToolCallUpdate: (event) => this.onToolUpdate(state, sessionId, event),
8262
8319
  onToolCallFinish: (event) => this.onToolFinish(state, sessionId, event),
8263
8320
  onRetry: (event) => this.onRetry(state, sessionId, event),
8321
+ onUsage: (event) => this.onModelCallUsage(state, stored, event),
8264
8322
  };
8265
8323
  stored.agentSession = await this.withCompactorContext(
8266
8324
  compactorContext,
@@ -8516,7 +8574,11 @@ export class FlexHarness<TScope = unknown> {
8516
8574
  const message = status === 'cancelled' ? repairCancellationMessage : externalErrorFallback.message;
8517
8575
  userMessage.error = message;
8518
8576
  assistantMessage.error = message;
8519
- delete assistantMessage.usage;
8577
+ if (stage?.usage) {
8578
+ assistantMessage.usage = cloneSerializable(stage.usage);
8579
+ } else {
8580
+ delete assistantMessage.usage;
8581
+ }
8520
8582
  }
8521
8583
  return {
8522
8584
  runId,
@@ -8524,7 +8586,7 @@ export class FlexHarness<TScope = unknown> {
8524
8586
  userMessage,
8525
8587
  assistantMessage,
8526
8588
  ...(status === 'completed' && stage?.model ? { model: stage.model } : {}),
8527
- ...(status === 'completed' && stage?.usage ? { usage: stage.usage } : {}),
8589
+ ...(stage?.usage ? { usage: stage.usage } : {}),
8528
8590
  ...(status === 'completed' && stage?.finishReason ? { finishReason: stage.finishReason } : {}),
8529
8591
  ...(status === 'completed' && stage?.steps !== undefined ? { steps: stage.steps } : {}),
8530
8592
  };
package/ts/interfaces.ts CHANGED
@@ -25,6 +25,12 @@ export type TFlexAgentToolCallFinishEvent = Parameters<
25
25
  /** A usage or rate limit a model provider reported; `resetsAt` is in epoch milliseconds. */
26
26
  export type TFlexModelLimit = plugins.IModelLimitInfo;
27
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;
28
34
 
29
35
  export interface IFlexTextPromptPart {
30
36
  type: 'text';
@@ -126,6 +132,11 @@ export interface IFlexMessage {
126
132
  completedAt?: string;
127
133
  parts: TFlexMessagePart[];
128
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
+ */
129
140
  usage?: IFlexUsage;
130
141
  error?: string;
131
142
  /** The model limit that failed this run, next to its error. */
@@ -464,6 +475,7 @@ export interface IFlexTerminalProjection {
464
475
  userMessage: IFlexMessage;
465
476
  assistantMessage: IFlexMessage;
466
477
  model?: IFlexModelIdentity;
478
+ /** The usage the provider reported for the run's model calls, whatever the status. */
467
479
  usage?: IFlexUsage;
468
480
  finishReason?: string;
469
481
  steps?: number;
@@ -1244,6 +1256,18 @@ export interface IFlexRunRetryingEvent extends IFlexEventBase {
1244
1256
  readonly reason: TFlexRetryReason;
1245
1257
  }
1246
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
+
1247
1271
  export interface IFlexPromptQueueEvent extends IFlexEventBase {
1248
1272
  readonly type: 'prompt.queued' | 'prompt.started' | 'prompt.running' | 'prompt.finished';
1249
1273
  readonly queueId: string;
@@ -1276,6 +1300,7 @@ export type TFlexHarnessEvent =
1276
1300
  | IFlexPermissionResolvedEvent
1277
1301
  | IFlexRunFinishedEvent
1278
1302
  | IFlexRunRetryingEvent
1303
+ | TFlexModelUsageEvent
1279
1304
  | IFlexPromptQueueEvent
1280
1305
  | IFlexSessionHistoryChangedEvent;
1281
1306
 
package/ts/plugins.ts CHANGED
@@ -63,6 +63,8 @@ export type {
63
63
  TAgentContextCompactor,
64
64
  TAgentEvent,
65
65
  TAgentGenerationOutcome,
66
+ TAgentModelCallUsage,
67
+ TAgentModelCallUsageEvent,
66
68
  TAgentRetryReason,
67
69
  TAgentToolCallFinishEvent,
68
70
  TAgentToolExecutionReconciliationOptions,
@@ -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';
@@ -24,6 +24,13 @@ When constructing the summary, try to stick to this template:
24
24
 
25
25
  export interface ICompactMessagesOptions {
26
26
  abortSignal?: AbortSignal;
27
+ /**
28
+ * Receives the usage of every model call the compaction makes, once per call: `reported` with the
29
+ * provider's usage, or `unreported` for a call that failed or was aborted, retried attempts
30
+ * included. A context compactor passes its own options through, so the calls reach the session's
31
+ * `onUsage`. Must not throw.
32
+ */
33
+ reportUsage?: plugins.TAgentModelCallUsageReporter;
27
34
  }
28
35
 
29
36
  /**
@@ -32,7 +39,7 @@ export interface ICompactMessagesOptions {
32
39
  *
33
40
  * @param model The same model used by runAgent, or a cheaper small model
34
41
  * @param messages The full message history that overflowed
35
- * @param options Invocation options, including cancellation
42
+ * @param options Invocation options, including cancellation and usage reporting
36
43
  * @returns A minimal ModelMessage[] containing the summary as context
37
44
  */
38
45
  export async function compactMessages(
@@ -40,20 +47,60 @@ export async function compactMessages(
40
47
  messages: plugins.ModelMessage[],
41
48
  options: ICompactMessagesOptions = {},
42
49
  ): Promise<plugins.ModelMessage[]> {
43
- const result = await plugins.generateText({
50
+ const prompt: plugins.ModelMessage[] = [
51
+ ...messages,
52
+ { role: 'user', content: COMPACTION_PROMPT },
53
+ ];
54
+ const reportUsage = options.reportUsage;
55
+ if (!reportUsage) {
56
+ const result = await plugins.generateText({
57
+ model,
58
+ abortSignal: options.abortSignal,
59
+ allowSystemInMessages: true,
60
+ messages: prompt,
61
+ });
62
+ return summaryMessages(result.text);
63
+ }
64
+ const modelCalls = new plugins.AgentModelCallUsageRecorder(model, reportUsage);
65
+ // Each attempt, retries included, is one model call: it starts in the middleware, which also
66
+ // settles an attempt that throws; a call whose response arrives ends in onLanguageModelCallEnd.
67
+ const observedModel = plugins.wrapLanguageModel({
44
68
  model,
45
- abortSignal: options.abortSignal,
46
- allowSystemInMessages: true,
47
- messages: [
48
- ...messages,
49
- { role: 'user', content: COMPACTION_PROMPT },
50
- ],
69
+ middleware: {
70
+ wrapGenerate: async ({ doGenerate, params }) => {
71
+ modelCalls.start();
72
+ try {
73
+ return await doGenerate();
74
+ } catch (error) {
75
+ modelCalls.settle(params.abortSignal?.aborted ? 'aborted' : 'failed');
76
+ throw error;
77
+ }
78
+ },
79
+ },
51
80
  });
81
+ try {
82
+ const result = await plugins.generateText({
83
+ model: observedModel,
84
+ abortSignal: options.abortSignal,
85
+ allowSystemInMessages: true,
86
+ messages: prompt,
87
+ onLanguageModelCallEnd: ({ modelId, usage }) => {
88
+ modelCalls.end(modelId, usage);
89
+ },
90
+ });
91
+ modelCalls.settle('missing');
92
+ return summaryMessages(result.text);
93
+ } catch (error) {
94
+ modelCalls.settle(options.abortSignal?.aborted ? 'aborted' : 'failed');
95
+ throw error;
96
+ }
97
+ }
52
98
 
99
+ function summaryMessages(summary: string): plugins.ModelMessage[] {
53
100
  return [
54
101
  {
55
102
  role: 'user',
56
- content: `[Previous conversation summary]\n\n${result.text}\n\n[End of summary. Continue from here.]`,
103
+ content: `[Previous conversation summary]\n\n${summary}\n\n[End of summary. Continue from here.]`,
57
104
  },
58
105
  ];
59
106
  }