@modelprofile.com/flexharness 10.13.0 → 10.14.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.codesandbox.d.ts +39 -0
- package/dist_ts/classes.codesandbox.js +539 -0
- package/dist_ts/classes.flexharness.d.ts +10 -0
- package/dist_ts/classes.flexharness.js +145 -15
- package/dist_ts/classes.toolcatalog.d.ts +14 -4
- package/dist_ts/classes.toolcatalog.js +80 -18
- package/dist_ts/interfaces.d.ts +88 -2
- package/dist_ts/interfaces.js +22 -1
- package/dist_ts/plugins.d.ts +62 -1
- package/dist_ts/plugins.js +15 -1
- package/dist_ts/utils.harnessoptions.d.ts +7 -2
- package/dist_ts/utils.harnessoptions.js +37 -9
- package/dist_ts/utils.json.js +3 -1
- package/dist_ts/utils.tooloutput.d.ts +2 -0
- package/dist_ts/utils.tooloutput.js +11 -1
- package/dist_ts_compaction/plugins.d.ts +3 -3
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.d.ts +10 -1
- package/dist_ts_compaction/smartagent.compaction.js +79 -38
- package/package.json +16 -4
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +565 -0
- package/ts/classes.flexharness.ts +148 -14
- package/ts/classes.toolcatalog.ts +94 -13
- package/ts/interfaces.ts +119 -2
- package/ts/plugins.ts +79 -0
- package/ts/readme.md +1 -1
- package/ts/utils.harnessoptions.ts +45 -12
- package/ts/utils.json.ts +2 -0
- package/ts/utils.tooloutput.ts +9 -0
- package/ts_compaction/plugins.ts +3 -3
- package/ts_compaction/smartagent.compaction.ts +90 -34
package/ts/plugins.ts
CHANGED
|
@@ -13,6 +13,84 @@ import { Validator as JsonSchemaValidator } from '@cfworker/json-schema';
|
|
|
13
13
|
export { asSchema, jsonSchema, JsonSchemaValidator };
|
|
14
14
|
export type { FlexibleSchema, JSONSchema7, ModelMessage, Schema } from 'ai';
|
|
15
15
|
|
|
16
|
+
// optional code mode peers: QuickJS-ng compiled to WebAssembly, and amaro to strip TypeScript types
|
|
17
|
+
|
|
18
|
+
/** A value inside a QuickJS context; the host disposes every handle it owns. */
|
|
19
|
+
export interface IQuickJsHandle {
|
|
20
|
+
readonly alive: boolean;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type TQuickJsResult =
|
|
25
|
+
| { value: IQuickJsHandle; error?: undefined }
|
|
26
|
+
| { error: IQuickJsHandle; value?: undefined };
|
|
27
|
+
|
|
28
|
+
export interface IQuickJsDeferredPromise {
|
|
29
|
+
readonly handle: IQuickJsHandle;
|
|
30
|
+
readonly alive: boolean;
|
|
31
|
+
resolve(value?: IQuickJsHandle): void;
|
|
32
|
+
reject(value?: IQuickJsHandle): void;
|
|
33
|
+
dispose(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IQuickJsContext {
|
|
37
|
+
readonly global: IQuickJsHandle;
|
|
38
|
+
newString(value: string): IQuickJsHandle;
|
|
39
|
+
getString(handle: IQuickJsHandle): string;
|
|
40
|
+
newFunction(name: string, implementation: (...args: IQuickJsHandle[]) => IQuickJsHandle | void): IQuickJsHandle;
|
|
41
|
+
setProp(target: IQuickJsHandle, key: string, value: IQuickJsHandle): void;
|
|
42
|
+
newPromise(): IQuickJsDeferredPromise;
|
|
43
|
+
newError(message: string): IQuickJsHandle;
|
|
44
|
+
evalCode(code: string, filename?: string): TQuickJsResult;
|
|
45
|
+
resolvePromise(handle: IQuickJsHandle): Promise<TQuickJsResult>;
|
|
46
|
+
dump(handle: IQuickJsHandle): unknown;
|
|
47
|
+
dispose(): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface IQuickJsRuntime {
|
|
51
|
+
readonly alive: boolean;
|
|
52
|
+
setMemoryLimit(bytes: number): void;
|
|
53
|
+
setMaxStackSize(bytes: number): void;
|
|
54
|
+
setInterruptHandler(handler: () => boolean): void;
|
|
55
|
+
newContext(): IQuickJsContext;
|
|
56
|
+
/** Runs the queued promise jobs; `error` is an exception none of them caught. */
|
|
57
|
+
executePendingJobs(): { error?: IQuickJsHandle };
|
|
58
|
+
dispose(): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface IQuickJsModule {
|
|
62
|
+
newRuntime(): IQuickJsRuntime;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A failed strip, as amaro reports it. */
|
|
66
|
+
export interface IAmaroError {
|
|
67
|
+
code?: string;
|
|
68
|
+
message?: string;
|
|
69
|
+
startLine?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ICodeSandboxEngine {
|
|
73
|
+
/** A new WebAssembly instance of QuickJS-ng, with a memory of its own. */
|
|
74
|
+
newModule(): Promise<IQuickJsModule>;
|
|
75
|
+
/** Removes TypeScript type syntax, keeping every line where it was. */
|
|
76
|
+
stripTypes(source: string): string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Loads QuickJS and amaro, the optional peers code mode needs. */
|
|
80
|
+
export const importCodeSandboxEngine = async (): Promise<ICodeSandboxEngine> => {
|
|
81
|
+
const [core, variant, amaro] = await Promise.all([
|
|
82
|
+
import('quickjs-emscripten-core'),
|
|
83
|
+
import('@jitl/quickjs-ng-wasmfile-release-sync'),
|
|
84
|
+
import('amaro'),
|
|
85
|
+
]);
|
|
86
|
+
// The variant's typings resolve as CommonJS; at run time the default export is the variant.
|
|
87
|
+
const quickjsVariant = (variant as { default: unknown }).default as Parameters<typeof core.newQuickJSWASMModuleFromVariant>[0];
|
|
88
|
+
return {
|
|
89
|
+
newModule: async () => await core.newQuickJSWASMModuleFromVariant(quickjsVariant) as unknown as IQuickJsModule,
|
|
90
|
+
stripTypes: (source) => amaro.transformSync(source, { mode: 'strip-only' }).code,
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
16
94
|
// @push.rocks scope
|
|
17
95
|
import {
|
|
18
96
|
AgentEventStoreConflictError,
|
|
@@ -80,6 +158,7 @@ export type {
|
|
|
80
158
|
TAgentGenerationOutcome,
|
|
81
159
|
TAgentModelCallUsage,
|
|
82
160
|
TAgentModelCallUsageEvent,
|
|
161
|
+
TAgentObservedModelCallUsage,
|
|
83
162
|
TAgentRetryReason,
|
|
84
163
|
TAgentToolCallFinishEvent,
|
|
85
164
|
TAgentToolExecutionReconciliationOptions,
|
package/ts/readme.md
CHANGED
|
@@ -1026,7 +1026,7 @@ A `contextCompactor` states which model to compact with from the model the compa
|
|
|
1026
1026
|
|
|
1027
1027
|
A compactor uses `resolvedModel.model` directly, and resolves `modelHint` the way its model resolver resolves a prompt's `modelHint`. Only when neither member is present, because nothing dispatched states a model, does the host choose the compaction model itself.
|
|
1028
1028
|
|
|
1029
|
-
A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, so a compactor that passes its options through
|
|
1029
|
+
A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. A `context-budget` compaction on a provider that caches the prefix also carries the turns' `tools`. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, with the tools' definitions but none of their code, so a compactor that passes its options through repeats the run's prefix and reads it from the provider's prompt cache; definitions that outweigh the history nine times are left out. Without `system` it sends its own instructions.
|
|
1030
1030
|
|
|
1031
1031
|
```typescript
|
|
1032
1032
|
agentSessionPolicy: {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { FlexHarnessValidationError } from './errors.js';
|
|
2
2
|
import {
|
|
3
|
+
FLEX_CODE_MODE_DEFAULT_LIMITS,
|
|
4
|
+
FLEX_CODE_MODE_MAXIMUM_LIMITS,
|
|
3
5
|
FLEX_REVERSION_DEFAULT_LIMITS,
|
|
4
6
|
FLEX_REVERSION_MAXIMUM_LIMITS,
|
|
5
7
|
} from './interfaces.js';
|
|
@@ -12,6 +14,7 @@ import type {
|
|
|
12
14
|
IFlexReversionLimits,
|
|
13
15
|
IFlexSlashCommandHandlerRegistration,
|
|
14
16
|
IFlexSlashCommandTemplateRegistration,
|
|
17
|
+
IFlexCodeModeLimits,
|
|
15
18
|
IFlexSubagentDefinition,
|
|
16
19
|
IFlexToolCatalogOptions,
|
|
17
20
|
TFlexSlashCommandRegistration,
|
|
@@ -157,25 +160,55 @@ export function normalizeSubagents(
|
|
|
157
160
|
return Object.freeze(normalized);
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
export interface INormalizedToolCatalogOptions {
|
|
164
|
+
readonly declarations: 'typescript' | 'json-schema';
|
|
165
|
+
/** The limits of `run_code`; absent while code mode is off. */
|
|
166
|
+
readonly codeMode?: Readonly<IFlexCodeModeLimits>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const isPlainOptions = (value: unknown): value is Record<string, unknown> =>
|
|
170
|
+
!!value
|
|
171
|
+
&& typeof value === 'object'
|
|
172
|
+
&& !Array.isArray(value)
|
|
173
|
+
&& [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
174
|
+
|
|
160
175
|
export function normalizeToolCatalogOptions(
|
|
161
|
-
options:
|
|
162
|
-
):
|
|
176
|
+
options: IFlexToolCatalogOptions | undefined,
|
|
177
|
+
): INormalizedToolCatalogOptions {
|
|
163
178
|
if (options === undefined) return Object.freeze({ declarations: 'typescript' });
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|| typeof options !== 'object'
|
|
167
|
-
|| Array.isArray(options)
|
|
168
|
-
|| ![Object.prototype, null].includes(Object.getPrototypeOf(options))
|
|
169
|
-
) {
|
|
170
|
-
throw new FlexHarnessValidationError('toolCatalog must be a plain object.');
|
|
171
|
-
}
|
|
172
|
-
const unsupported = Object.keys(options).find((key) => key !== 'declarations');
|
|
179
|
+
if (!isPlainOptions(options)) throw new FlexHarnessValidationError('toolCatalog must be a plain object.');
|
|
180
|
+
const unsupported = Object.keys(options).find((key) => key !== 'declarations' && key !== 'codeMode');
|
|
173
181
|
if (unsupported) throw new FlexHarnessValidationError(`toolCatalog does not support "${unsupported}".`);
|
|
174
182
|
const declarations = options.declarations ?? 'typescript';
|
|
175
183
|
if (declarations !== 'typescript' && declarations !== 'json-schema') {
|
|
176
184
|
throw new FlexHarnessValidationError('toolCatalog.declarations must be "typescript" or "json-schema".');
|
|
177
185
|
}
|
|
178
|
-
|
|
186
|
+
const codeMode = normalizeCodeModeLimits(options.codeMode);
|
|
187
|
+
return Object.freeze({ declarations, ...(codeMode ? { codeMode } : {}) });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeCodeModeLimits(codeMode: unknown): Readonly<IFlexCodeModeLimits> | undefined {
|
|
191
|
+
if (codeMode === undefined || codeMode === false) return undefined;
|
|
192
|
+
if (codeMode === true) return FLEX_CODE_MODE_DEFAULT_LIMITS;
|
|
193
|
+
if (!isPlainOptions(codeMode)) {
|
|
194
|
+
throw new FlexHarnessValidationError('toolCatalog.codeMode must be a boolean or a plain object.');
|
|
195
|
+
}
|
|
196
|
+
const unsupported = Object.keys(codeMode).find((key) => key !== 'limits');
|
|
197
|
+
if (unsupported) throw new FlexHarnessValidationError(`toolCatalog.codeMode does not support "${unsupported}".`);
|
|
198
|
+
const limits = codeMode.limits ?? {};
|
|
199
|
+
if (!isPlainOptions(limits)) throw new FlexHarnessValidationError('toolCatalog.codeMode.limits must be a plain object.');
|
|
200
|
+
const resolved: Record<string, number> = { ...FLEX_CODE_MODE_DEFAULT_LIMITS };
|
|
201
|
+
for (const [key, value] of Object.entries(limits)) {
|
|
202
|
+
if (!Object.prototype.hasOwnProperty.call(FLEX_CODE_MODE_MAXIMUM_LIMITS, key)) {
|
|
203
|
+
throw new FlexHarnessValidationError(`toolCatalog.codeMode.limits does not support "${key}".`);
|
|
204
|
+
}
|
|
205
|
+
const maximum = FLEX_CODE_MODE_MAXIMUM_LIMITS[key as keyof IFlexCodeModeLimits];
|
|
206
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
207
|
+
throw new FlexHarnessValidationError(`toolCatalog.codeMode.limits.${key} must be an integer from 1 through ${maximum}.`);
|
|
208
|
+
}
|
|
209
|
+
resolved[key] = value;
|
|
210
|
+
}
|
|
211
|
+
return Object.freeze(resolved as unknown as IFlexCodeModeLimits);
|
|
179
212
|
}
|
|
180
213
|
|
|
181
214
|
export function normalizeBuiltInTools(
|
package/ts/utils.json.ts
CHANGED
|
@@ -785,6 +785,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
785
785
|
'output',
|
|
786
786
|
'error',
|
|
787
787
|
'childSessionId',
|
|
788
|
+
'parentToolCallId',
|
|
788
789
|
'model',
|
|
789
790
|
'startedAt',
|
|
790
791
|
'completedAt',
|
|
@@ -801,6 +802,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
801
802
|
}
|
|
802
803
|
requireOptionalString(part.error, `${partPath}.error`);
|
|
803
804
|
requireOptionalString(part.childSessionId, `${partPath}.childSessionId`);
|
|
805
|
+
requireOptionalString(part.parentToolCallId, `${partPath}.parentToolCallId`);
|
|
804
806
|
if (part.model !== undefined) validateModel(part.model, `${partPath}.model`);
|
|
805
807
|
requireOptionalString(part.startedAt, `${partPath}.startedAt`);
|
|
806
808
|
requireOptionalString(part.completedAt, `${partPath}.completedAt`);
|
package/ts/utils.tooloutput.ts
CHANGED
|
@@ -25,6 +25,15 @@ async function* normalizeAsyncIterable(
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** A tool output's final value: the value itself, or the last value an async iterable yields. */
|
|
29
|
+
export async function settleToolOutput(output: unknown): Promise<unknown> {
|
|
30
|
+
const value = await output;
|
|
31
|
+
if (!isAsyncIterable(value)) return value;
|
|
32
|
+
let last: unknown;
|
|
33
|
+
for await (const item of value) last = item;
|
|
34
|
+
return last;
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
export function wrapToolSet(
|
|
29
38
|
tools: TFlexAgentToolSet,
|
|
30
39
|
limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
|
package/ts_compaction/plugins.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { streamText, wrapLanguageModel } from 'ai';
|
|
1
|
+
import { asSchema, streamText, wrapLanguageModel } from 'ai';
|
|
2
2
|
|
|
3
|
-
export { streamText, wrapLanguageModel };
|
|
3
|
+
export { asSchema, streamText, wrapLanguageModel };
|
|
4
4
|
|
|
5
|
-
export type { ModelMessage } from 'ai';
|
|
5
|
+
export type { ModelMessage, ToolSet } from 'ai';
|
|
6
6
|
export type { TSmartAiLanguageModel } from '@modelprofile.com/flexharness-models';
|
|
7
7
|
|
|
8
8
|
import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
|
|
@@ -25,6 +25,9 @@ When constructing the summary, try to stick to this template:
|
|
|
25
25
|
/** The instructions of a summary call whose caller names none of the conversation's own. */
|
|
26
26
|
const COMPACTION_INSTRUCTIONS = 'You summarize a conversation so that another agent can continue its work.';
|
|
27
27
|
|
|
28
|
+
/** Added to the summary request when it carries the conversation's tools. */
|
|
29
|
+
const NO_TOOL_CALLS = 'Do not call any tool. Reply with the summary text only.';
|
|
30
|
+
|
|
28
31
|
export interface ICompactMessagesOptions {
|
|
29
32
|
abortSignal?: AbortSignal;
|
|
30
33
|
/**
|
|
@@ -45,13 +48,51 @@ export interface ICompactMessagesOptions {
|
|
|
45
48
|
* options' `providerOptions` through.
|
|
46
49
|
*/
|
|
47
50
|
providerOptions?: plugins.ProviderOptions;
|
|
51
|
+
/**
|
|
52
|
+
* The tools the conversation's turns are sent with. The summary call carries their definitions,
|
|
53
|
+
* none of their code, so its request repeats the turns' prefix and reads it from the provider's
|
|
54
|
+
* prompt cache, unless the definitions outweigh the history nine times, when reading them would
|
|
55
|
+
* cost more than the history saves. A context compactor passes its options' `tools` through.
|
|
56
|
+
*/
|
|
57
|
+
tools?: plugins.ToolSet;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether the summary request should carry the turns' tool definitions. A cached prefix is read at
|
|
62
|
+
* about a tenth of the input price (Anthropic and OpenAI), so repeating the turns' prefix costs about
|
|
63
|
+
* 0.1 x (tools + history), while a request without the tools reads nothing and pays the history in
|
|
64
|
+
* full: the tools pay their way while they are less than nine times the history.
|
|
65
|
+
*/
|
|
66
|
+
async function toolsRepayTheirPrefix(tools: plugins.ToolSet, messages: plugins.ModelMessage[]): Promise<boolean> {
|
|
67
|
+
let toolBytes = 0;
|
|
68
|
+
for (const [name, definition] of Object.entries(tools)) {
|
|
69
|
+
const schema = definition.type === 'provider' ? definition.args : await plugins.asSchema(definition.inputSchema).jsonSchema;
|
|
70
|
+
toolBytes += name.length + JSON.stringify([definition.description ?? '', schema ?? null]).length;
|
|
71
|
+
}
|
|
72
|
+
return toolBytes <= 9 * JSON.stringify(messages).length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A tool set's definitions without anything that runs: no `execute`, no input callbacks. */
|
|
76
|
+
function toolDefinitions(tools: plugins.ToolSet): plugins.ToolSet {
|
|
77
|
+
return Object.fromEntries(Object.entries(tools).map(([name, definition]) => {
|
|
78
|
+
const {
|
|
79
|
+
execute: _execute,
|
|
80
|
+
onInputStart: _onInputStart,
|
|
81
|
+
onInputDelta: _onInputDelta,
|
|
82
|
+
onInputAvailable: _onInputAvailable,
|
|
83
|
+
...rest
|
|
84
|
+
} = definition as plugins.ToolSet[string] & Record<string, unknown>;
|
|
85
|
+
return [name, rest as plugins.ToolSet[string]];
|
|
86
|
+
}));
|
|
48
87
|
}
|
|
49
88
|
|
|
50
89
|
/**
|
|
51
90
|
* Compacts a message history into a summary.
|
|
52
91
|
*
|
|
53
92
|
* The summary is one streamed, instructed model call, the shape every agent turn has, so it reaches
|
|
54
|
-
* a provider that serves only that shape (the ChatGPT Codex backend) the way the turns do.
|
|
93
|
+
* a provider that serves only that shape (the ChatGPT Codex backend) the way the turns do. With the
|
|
94
|
+
* turns' `tools` it also repeats their prefix, which the provider has cached; a model that answers
|
|
95
|
+
* it with a tool call instead of a summary is asked once more with tool use turned off.
|
|
55
96
|
*
|
|
56
97
|
* @param model The same model used by runAgent, or a cheaper small model
|
|
57
98
|
* @param messages The full message history that overflowed
|
|
@@ -63,9 +104,12 @@ export async function compactMessages(
|
|
|
63
104
|
messages: plugins.ModelMessage[],
|
|
64
105
|
options: ICompactMessagesOptions = {},
|
|
65
106
|
): Promise<plugins.ModelMessage[]> {
|
|
107
|
+
const tools = options.tools && Object.keys(options.tools).length > 0 && await toolsRepayTheirPrefix(options.tools, messages)
|
|
108
|
+
? toolDefinitions(options.tools)
|
|
109
|
+
: undefined;
|
|
66
110
|
const prompt: plugins.ModelMessage[] = [
|
|
67
111
|
...messages,
|
|
68
|
-
{ role: 'user', content: COMPACTION_PROMPT },
|
|
112
|
+
{ role: 'user', content: tools ? `${COMPACTION_PROMPT}\n\n${NO_TOOL_CALLS}` : COMPACTION_PROMPT },
|
|
69
113
|
];
|
|
70
114
|
const reportUsage = options.reportUsage;
|
|
71
115
|
const modelCalls = reportUsage
|
|
@@ -89,38 +133,50 @@ export async function compactMessages(
|
|
|
89
133
|
},
|
|
90
134
|
})
|
|
91
135
|
: model;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
// The summary is asked for in the shape of a turn, the tools offered as the turns offer them. A
|
|
137
|
+
// model that calls a tool instead of answering is asked again with tool use turned off, which
|
|
138
|
+
// keeps the tool definitions and so still the cached tools and instructions.
|
|
139
|
+
const summarize = async (toolChoice?: 'none'): Promise<{ summary: string; calledTools: boolean }> => {
|
|
140
|
+
let streamError: unknown;
|
|
141
|
+
try {
|
|
142
|
+
const result = plugins.streamText({
|
|
143
|
+
model: observedModel,
|
|
144
|
+
instructions: options.system ?? COMPACTION_INSTRUCTIONS,
|
|
145
|
+
messages: prompt,
|
|
146
|
+
allowSystemInMessages: true,
|
|
147
|
+
...(tools ? { tools, ...(toolChoice ? { toolChoice } : {}) } : {}),
|
|
148
|
+
...(options.providerOptions ? { providerOptions: options.providerOptions } : {}),
|
|
149
|
+
abortSignal: options.abortSignal,
|
|
150
|
+
onError: ({ error }) => {
|
|
151
|
+
streamError ??= error;
|
|
152
|
+
},
|
|
153
|
+
...(modelCalls
|
|
154
|
+
? {
|
|
155
|
+
onLanguageModelCallEnd: ({ modelId, usage }) => {
|
|
156
|
+
modelCalls.end(modelId, usage);
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
: {}),
|
|
160
|
+
});
|
|
161
|
+
let summary = '';
|
|
162
|
+
for await (const delta of result.textStream) summary += delta;
|
|
163
|
+
// A stream that fails delivers its error to onError and ends, and an aborted stream just ends;
|
|
164
|
+
// either way the summary is incomplete and must not replace the history.
|
|
165
|
+
if (streamError !== undefined) throw streamError;
|
|
166
|
+
options.abortSignal?.throwIfAborted();
|
|
167
|
+
modelCalls?.settle('missing');
|
|
168
|
+
return { summary, calledTools: tools !== undefined && (await result.toolCalls).length > 0 };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
modelCalls?.settle(options.abortSignal?.aborted ? 'aborted' : 'failed');
|
|
171
|
+
throw streamError ?? error;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
// A provider-executed tool, such as a web search, would run on the provider's side before the
|
|
175
|
+
// retry could prevent it: with one in the set, tool use is off from the start.
|
|
176
|
+
const providerExecuted = tools !== undefined && Object.values(tools).some((definition) => definition.type === 'provider');
|
|
177
|
+
let { summary, calledTools } = await summarize(providerExecuted ? 'none' : undefined);
|
|
178
|
+
if (calledTools && !summary.trim()) ({ summary } = await summarize('none'));
|
|
179
|
+
return summaryMessages(summary);
|
|
124
180
|
}
|
|
125
181
|
|
|
126
182
|
function summaryMessages(summary: string): plugins.ModelMessage[] {
|