@gram-ai/elements 1.28.0 → 1.29.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/elements.cjs +1 -1
- package/dist/elements.js +1 -1
- package/dist/{index-CtZz13Cf.js → index-BzA55RRF.js} +11741 -11557
- package/dist/index-BzA55RRF.js.map +1 -0
- package/dist/{index-BmTGnEaV.cjs → index-CgO7wXs-.cjs} +52 -48
- package/dist/index-CgO7wXs-.cjs.map +1 -0
- package/dist/lib/contextCompaction.d.ts +58 -0
- package/dist/lib/contextCompaction.test.d.ts +1 -0
- package/dist/lib/errorTracking.config.d.ts +2 -0
- package/dist/lib/tools.byte-cap.test.d.ts +1 -0
- package/dist/lib/tools.d.ts +19 -0
- package/dist/lib/tools.test.d.ts +1 -0
- package/dist/{profiler-Ccma0l1p.js → profiler-BPCxiY-X.js} +2 -2
- package/dist/{profiler-Ccma0l1p.js.map → profiler-BPCxiY-X.js.map} +1 -1
- package/dist/{profiler-CjNa3A1d.cjs → profiler-BmAwBXpj.cjs} +2 -2
- package/dist/{profiler-CjNa3A1d.cjs.map → profiler-BmAwBXpj.cjs.map} +1 -1
- package/dist/{startRecording-jSovclaq.cjs → startRecording-B0Xe2DOI.cjs} +2 -2
- package/dist/{startRecording-jSovclaq.cjs.map → startRecording-B0Xe2DOI.cjs.map} +1 -1
- package/dist/{startRecording-DAURU74n.js → startRecording-DXGt4fON.js} +2 -2
- package/dist/{startRecording-DAURU74n.js.map → startRecording-DXGt4fON.js.map} +1 -1
- package/dist/types/index.d.ts +49 -0
- package/package.json +1 -1
- package/src/contexts/ElementsProvider.tsx +50 -5
- package/src/lib/contextCompaction.test.ts +201 -0
- package/src/lib/contextCompaction.ts +211 -0
- package/src/lib/errorTracking.config.ts +2 -0
- package/src/lib/errorTracking.ts +1 -1
- package/src/lib/tools.byte-cap.test.ts +132 -0
- package/src/lib/tools.test.ts +259 -0
- package/src/lib/tools.ts +122 -0
- package/src/types/index.ts +55 -0
- package/dist/index-BmTGnEaV.cjs.map +0 -1
- package/dist/index-CtZz13Cf.js.map +0 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
convertToModelMessages,
|
|
4
|
+
isToolUIPart,
|
|
5
|
+
jsonSchema,
|
|
6
|
+
lastAssistantMessageIsCompleteWithToolCalls,
|
|
7
|
+
readUIMessageStream,
|
|
8
|
+
stepCountIs,
|
|
9
|
+
streamText,
|
|
10
|
+
type ToolSet,
|
|
11
|
+
type UIMessage,
|
|
12
|
+
type UIMessagePart,
|
|
13
|
+
} from "ai";
|
|
14
|
+
import { MockLanguageModelV2 } from "ai/test";
|
|
15
|
+
|
|
16
|
+
type MockStream = Extract<
|
|
17
|
+
NonNullable<
|
|
18
|
+
NonNullable<
|
|
19
|
+
ConstructorParameters<typeof MockLanguageModelV2>[0]
|
|
20
|
+
>["doStream"]
|
|
21
|
+
>,
|
|
22
|
+
(...a: never[]) => PromiseLike<{ stream: ReadableStream<unknown> }>
|
|
23
|
+
>;
|
|
24
|
+
type StreamPart =
|
|
25
|
+
Awaited<ReturnType<MockStream>>["stream"] extends ReadableStream<infer T>
|
|
26
|
+
? T
|
|
27
|
+
: never;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Repro for the assistants-onboarding "Skip bugged state":
|
|
31
|
+
*
|
|
32
|
+
* 1. Assistant calls a frontend tool (e.g. `request_environment_secrets`) that
|
|
33
|
+
* renders a form with a Skip button.
|
|
34
|
+
* 2. User clicks Skip. The form calls `draft.resolvePending(toolCallId, { cancelled: true })`.
|
|
35
|
+
* 3. Expected: tool-result is patched onto the message, the agent continues,
|
|
36
|
+
* chat returns to a ready state.
|
|
37
|
+
* 4. Observed (pre-fix): the chat stayed stuck — the next user message landed
|
|
38
|
+
* with an invalid tool sequence and the provider rejected it with a
|
|
39
|
+
* "message needing to be sent with role: assistant"-shaped error.
|
|
40
|
+
*
|
|
41
|
+
* `streamText` runs without an `execute` for frontend tools: AI-SDK's
|
|
42
|
+
* `frontendTools()` helper strips execute so client-side logic can take over.
|
|
43
|
+
* The missing link on main was that the runtime patched in the tool result
|
|
44
|
+
* but nothing resumed the turn. The fix wires `sendAutomaticallyWhen:
|
|
45
|
+
* lastAssistantMessageIsCompleteWithToolCalls` into `useChatRuntime`, which
|
|
46
|
+
* flips that resume on.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
function toolCallChunks(opts: {
|
|
50
|
+
toolCallId: string;
|
|
51
|
+
toolName: string;
|
|
52
|
+
input: string;
|
|
53
|
+
}): StreamPart[] {
|
|
54
|
+
return [
|
|
55
|
+
{ type: "stream-start", warnings: [] },
|
|
56
|
+
{
|
|
57
|
+
type: "response-metadata",
|
|
58
|
+
id: "resp-1",
|
|
59
|
+
modelId: "m",
|
|
60
|
+
timestamp: new Date(0),
|
|
61
|
+
},
|
|
62
|
+
{ type: "tool-input-start", id: opts.toolCallId, toolName: opts.toolName },
|
|
63
|
+
{ type: "tool-input-delta", id: opts.toolCallId, delta: opts.input },
|
|
64
|
+
{ type: "tool-input-end", id: opts.toolCallId },
|
|
65
|
+
{
|
|
66
|
+
type: "tool-call",
|
|
67
|
+
toolCallId: opts.toolCallId,
|
|
68
|
+
toolName: opts.toolName,
|
|
69
|
+
input: opts.input,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "finish",
|
|
73
|
+
finishReason: "tool-calls",
|
|
74
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makeStream<T>(chunks: T[]): ReadableStream<T> {
|
|
80
|
+
return new ReadableStream({
|
|
81
|
+
start(controller) {
|
|
82
|
+
for (const c of chunks) controller.enqueue(c);
|
|
83
|
+
controller.close();
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function collectUIMessages(
|
|
89
|
+
stream: AsyncIterable<UIMessage>,
|
|
90
|
+
): Promise<UIMessage[]> {
|
|
91
|
+
const out: UIMessage[] = [];
|
|
92
|
+
for await (const msg of stream) {
|
|
93
|
+
const idx = out.findIndex((m) => m.id === msg.id);
|
|
94
|
+
if (idx >= 0) out[idx] = msg;
|
|
95
|
+
else out.push(msg);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function streamToolCallOnly(toolCallId: string): Promise<UIMessage[]> {
|
|
101
|
+
const toolsNoExecute = {
|
|
102
|
+
request_environment_secrets: {
|
|
103
|
+
description: "Ask the user to enter secrets for an env.",
|
|
104
|
+
inputSchema: jsonSchema({
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
keys: {
|
|
108
|
+
type: "array",
|
|
109
|
+
items: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: { name: { type: "string" } },
|
|
112
|
+
required: ["name"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ["keys"],
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
} as unknown as ToolSet;
|
|
120
|
+
|
|
121
|
+
const model = new MockLanguageModelV2({
|
|
122
|
+
doStream: async () => ({
|
|
123
|
+
stream: makeStream([
|
|
124
|
+
...toolCallChunks({
|
|
125
|
+
toolCallId,
|
|
126
|
+
toolName: "request_environment_secrets",
|
|
127
|
+
input: JSON.stringify({ keys: [{ name: "SLACK_BOT_TOKEN" }] }),
|
|
128
|
+
}),
|
|
129
|
+
]),
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = streamText({
|
|
134
|
+
model,
|
|
135
|
+
messages: [{ role: "user", content: "Set up Slack" }],
|
|
136
|
+
tools: toolsNoExecute,
|
|
137
|
+
stopWhen: stepCountIs(5),
|
|
138
|
+
});
|
|
139
|
+
return collectUIMessages(
|
|
140
|
+
readUIMessageStream({ stream: result.toUIMessageStream() }),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
describe("frontend tool Skip flow (sendAutomaticallyWhen fix)", () => {
|
|
145
|
+
it("without a tool-result, the message sequence is invalid — this is the bug we are fixing", async () => {
|
|
146
|
+
// Mirrors the elements flow on main: frontend tool has no execute inside
|
|
147
|
+
// streamText (it's run client-side by useToolInvocations). If nothing
|
|
148
|
+
// patches a tool-result onto the message, sending a follow-up user
|
|
149
|
+
// message produces an invalid sequence.
|
|
150
|
+
const toolCallId = "call_unresolved";
|
|
151
|
+
const messages = await streamToolCallOnly(toolCallId);
|
|
152
|
+
|
|
153
|
+
const assistant = messages.find((m) => m.role === "assistant")!;
|
|
154
|
+
const toolParts = (assistant.parts as UIMessagePart<never, never>[]).filter(
|
|
155
|
+
(p) => isToolUIPart(p),
|
|
156
|
+
);
|
|
157
|
+
expect(toolParts).toHaveLength(1);
|
|
158
|
+
expect((toolParts[0] as unknown as { state: string }).state).toBe(
|
|
159
|
+
"input-available",
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const follow: UIMessage[] = [
|
|
163
|
+
...messages,
|
|
164
|
+
{
|
|
165
|
+
id: "u2",
|
|
166
|
+
role: "user",
|
|
167
|
+
parts: [{ type: "text", text: "skip" }],
|
|
168
|
+
} as unknown as UIMessage,
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// Also: `lastAssistantMessageIsCompleteWithToolCalls` must return `false`
|
|
172
|
+
// here — there is no tool result, so the runtime should NOT auto-resume.
|
|
173
|
+
expect(lastAssistantMessageIsCompleteWithToolCalls({ messages })).toBe(
|
|
174
|
+
false,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// And the resulting model-message sequence contains a bogus `role: "tool"`
|
|
178
|
+
// with empty content — the provider will reject this as an invalid tool
|
|
179
|
+
// message, surfacing to the user as the "needs role: assistant" error.
|
|
180
|
+
const modelMsgs = convertToModelMessages(follow);
|
|
181
|
+
const assistantIdx = modelMsgs.findIndex(
|
|
182
|
+
(m) =>
|
|
183
|
+
m.role === "assistant" &&
|
|
184
|
+
Array.isArray(m.content) &&
|
|
185
|
+
(m.content as Array<{ type: string }>).some(
|
|
186
|
+
(c) => c.type === "tool-call",
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
expect(assistantIdx).toBeGreaterThanOrEqual(0);
|
|
190
|
+
expect(modelMsgs[assistantIdx + 1]?.role).toBe("tool");
|
|
191
|
+
expect(modelMsgs[assistantIdx + 1]?.content).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("once the tool-result is patched onto the message, sendAutomaticallyWhen fires and the sequence is valid", async () => {
|
|
195
|
+
// Simulates the full post-fix behaviour: `useToolInvocations` ran execute
|
|
196
|
+
// client-side and called `addToolResult`, which flips the tool part to
|
|
197
|
+
// `output-available`. With the result in place:
|
|
198
|
+
// - `lastAssistantMessageIsCompleteWithToolCalls` returns true, so the
|
|
199
|
+
// runtime re-issues the model turn (this is the 1-line fix).
|
|
200
|
+
// - `convertToModelMessages` produces a real `role: "tool"` message
|
|
201
|
+
// with the result, which the provider accepts.
|
|
202
|
+
const toolCallId = "call_resolved";
|
|
203
|
+
const rawMessages = await streamToolCallOnly(toolCallId);
|
|
204
|
+
|
|
205
|
+
// Patch in the tool-output-available state — this is the shape
|
|
206
|
+
// `chatHelpers.addToolResult` produces under the hood.
|
|
207
|
+
const patched: UIMessage[] = rawMessages.map((m) => {
|
|
208
|
+
if (m.role !== "assistant") return m;
|
|
209
|
+
return {
|
|
210
|
+
...m,
|
|
211
|
+
parts: (m.parts as Array<Record<string, unknown>>).map((p) =>
|
|
212
|
+
isToolUIPart(p as UIMessagePart<never, never>)
|
|
213
|
+
? {
|
|
214
|
+
...p,
|
|
215
|
+
state: "output-available",
|
|
216
|
+
output: { ok: true, cancelled: true },
|
|
217
|
+
}
|
|
218
|
+
: p,
|
|
219
|
+
),
|
|
220
|
+
} as UIMessage;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const toolPart = (
|
|
224
|
+
(
|
|
225
|
+
patched.find((m) => m.role === "assistant")!.parts as UIMessagePart<
|
|
226
|
+
never,
|
|
227
|
+
never
|
|
228
|
+
>[]
|
|
229
|
+
).filter((p) => isToolUIPart(p))[0] as unknown as { state: string }
|
|
230
|
+
).state;
|
|
231
|
+
expect(toolPart).toBe("output-available");
|
|
232
|
+
|
|
233
|
+
// Pre-condition for the fix: the runtime auto-resumes the turn.
|
|
234
|
+
expect(
|
|
235
|
+
lastAssistantMessageIsCompleteWithToolCalls({ messages: patched }),
|
|
236
|
+
).toBe(true);
|
|
237
|
+
|
|
238
|
+
// And the sequence handed to the model is well-formed (assistant
|
|
239
|
+
// tool-call is followed by a real role:"tool" message with a result).
|
|
240
|
+
const modelMsgs = convertToModelMessages(patched);
|
|
241
|
+
const assistantIdx = modelMsgs.findIndex(
|
|
242
|
+
(m) =>
|
|
243
|
+
m.role === "assistant" &&
|
|
244
|
+
Array.isArray(m.content) &&
|
|
245
|
+
(m.content as Array<{ type: string }>).some(
|
|
246
|
+
(c) => c.type === "tool-call",
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
const next = modelMsgs[assistantIdx + 1];
|
|
250
|
+
expect(next?.role).toBe("tool");
|
|
251
|
+
expect(Array.isArray(next?.content) && next.content.length).toBeGreaterThan(
|
|
252
|
+
0,
|
|
253
|
+
);
|
|
254
|
+
const toolResult = (
|
|
255
|
+
next?.content as Array<{ type: string; output?: { type?: string } }>
|
|
256
|
+
)[0];
|
|
257
|
+
expect(toolResult?.type).toBe("tool-result");
|
|
258
|
+
});
|
|
259
|
+
});
|
package/src/lib/tools.ts
CHANGED
|
@@ -156,6 +156,128 @@ export interface ApprovalHelpers {
|
|
|
156
156
|
whitelistTool: (toolName: string) => void;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Default head/tail split (bytes) when a tool result exceeds the cap. Head keeps
|
|
161
|
+
* early context (e.g. the preamble of a log query); tail keeps the most recent
|
|
162
|
+
* lines, which are usually the most relevant.
|
|
163
|
+
*/
|
|
164
|
+
const BYTE_CAP_HEAD_FRACTION = 0.9;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Truncates a single string to maxBytes using a head + tail preserving strategy
|
|
168
|
+
* when it exceeds the cap. Returns the original string when under the cap.
|
|
169
|
+
*/
|
|
170
|
+
export function truncateTextToByteCap(text: string, maxBytes: number): string {
|
|
171
|
+
if (maxBytes <= 0) return text;
|
|
172
|
+
const original = text;
|
|
173
|
+
// Work in UTF-8 bytes to match what OpenRouter counts.
|
|
174
|
+
const encoded = new TextEncoder().encode(original);
|
|
175
|
+
if (encoded.byteLength <= maxBytes) return original;
|
|
176
|
+
|
|
177
|
+
// Reserve room for the notice up-front so final output stays under maxBytes.
|
|
178
|
+
// Without this deduction, output would be head + notice + tail ≈ maxBytes
|
|
179
|
+
// + ~100 bytes, which silently overshoots the cap.
|
|
180
|
+
const notice = `\n\n[…tool output truncated from ${encoded.byteLength} bytes to ${maxBytes}; ask a narrower question to see more…]\n\n`;
|
|
181
|
+
const noticeBytes = new TextEncoder().encode(notice).byteLength;
|
|
182
|
+
const availableBytes = Math.max(0, maxBytes - noticeBytes);
|
|
183
|
+
|
|
184
|
+
const headBytes = Math.max(
|
|
185
|
+
0,
|
|
186
|
+
Math.floor(availableBytes * BYTE_CAP_HEAD_FRACTION),
|
|
187
|
+
);
|
|
188
|
+
const tailBytes = Math.max(0, availableBytes - headBytes);
|
|
189
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
190
|
+
const head = decoder.decode(encoded.slice(0, headBytes));
|
|
191
|
+
const tail =
|
|
192
|
+
tailBytes > 0
|
|
193
|
+
? decoder.decode(encoded.slice(encoded.byteLength - tailBytes))
|
|
194
|
+
: "";
|
|
195
|
+
|
|
196
|
+
return tail ? `${head}${notice}${tail}` : `${head}${notice}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Walks the shape returned by MCP/AI-SDK tool executors and truncates any
|
|
201
|
+
* over-sized text payload in place. Handles:
|
|
202
|
+
* - plain strings
|
|
203
|
+
* - { content: Array<{ type, text?, ... }>, isError? }
|
|
204
|
+
* Other shapes pass through untouched.
|
|
205
|
+
*/
|
|
206
|
+
export function capToolResultBytes(result: unknown, maxBytes: number): unknown {
|
|
207
|
+
if (maxBytes <= 0) return result;
|
|
208
|
+
|
|
209
|
+
if (typeof result === "string") {
|
|
210
|
+
return truncateTextToByteCap(result, maxBytes);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (result && typeof result === "object" && "content" in result) {
|
|
214
|
+
const r = result as {
|
|
215
|
+
content?: unknown;
|
|
216
|
+
isError?: boolean;
|
|
217
|
+
[k: string]: unknown;
|
|
218
|
+
};
|
|
219
|
+
if (Array.isArray(r.content)) {
|
|
220
|
+
const cappedContent = r.content.map((chunk) => {
|
|
221
|
+
if (
|
|
222
|
+
chunk &&
|
|
223
|
+
typeof chunk === "object" &&
|
|
224
|
+
(chunk as { type?: unknown }).type === "text" &&
|
|
225
|
+
typeof (chunk as { text?: unknown }).text === "string"
|
|
226
|
+
) {
|
|
227
|
+
return {
|
|
228
|
+
...(chunk as Record<string, unknown>),
|
|
229
|
+
text: truncateTextToByteCap(
|
|
230
|
+
(chunk as { text: string }).text,
|
|
231
|
+
maxBytes,
|
|
232
|
+
),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return chunk;
|
|
236
|
+
});
|
|
237
|
+
return { ...r, content: cappedContent };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Wraps tools so that oversized results are truncated before they reach the
|
|
246
|
+
* conversation history. Tools whose result fits under the cap pass through
|
|
247
|
+
* untouched. Composes cleanly before or after wrapToolsWithApproval.
|
|
248
|
+
*/
|
|
249
|
+
export function wrapToolsWithByteCap(
|
|
250
|
+
tools: ToolSet,
|
|
251
|
+
maxBytes: number | undefined,
|
|
252
|
+
): ToolSet {
|
|
253
|
+
if (!maxBytes || maxBytes <= 0) {
|
|
254
|
+
return tools;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return Object.fromEntries(
|
|
258
|
+
Object.entries(tools).map(([name, tool]) => {
|
|
259
|
+
const originalExecute = tool.execute;
|
|
260
|
+
if (!originalExecute) {
|
|
261
|
+
return [name, tool];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return [
|
|
265
|
+
name,
|
|
266
|
+
{
|
|
267
|
+
...tool,
|
|
268
|
+
execute: async (args: unknown, options?: ToolCallOptions) => {
|
|
269
|
+
const result = await originalExecute(
|
|
270
|
+
args,
|
|
271
|
+
options as Parameters<typeof originalExecute>[1],
|
|
272
|
+
);
|
|
273
|
+
return capToolResultBytes(result, maxBytes);
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
}),
|
|
278
|
+
) as ToolSet;
|
|
279
|
+
}
|
|
280
|
+
|
|
159
281
|
/**
|
|
160
282
|
* Wraps tools with approval logic based on the approval config.
|
|
161
283
|
*/
|
package/src/types/index.ts
CHANGED
|
@@ -268,6 +268,13 @@ export interface ElementsConfig {
|
|
|
268
268
|
*/
|
|
269
269
|
tools?: ToolsConfig;
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Configuration for automatic conversation compaction when the estimated
|
|
273
|
+
* input size approaches the model's context window. Defaults are safe for
|
|
274
|
+
* all models; override per-page to tighten or disable.
|
|
275
|
+
*/
|
|
276
|
+
contextCompaction?: ContextCompactionConfig;
|
|
277
|
+
|
|
271
278
|
/**
|
|
272
279
|
* Configuration for chat history and thread persistence.
|
|
273
280
|
* When enabled, conversations are saved and the thread list is shown.
|
|
@@ -690,6 +697,54 @@ export interface ToolsConfig {
|
|
|
690
697
|
* }
|
|
691
698
|
*/
|
|
692
699
|
toolsToInclude?: ToolsFilter;
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Maximum UTF-8 byte size for any single tool call's result. Results larger
|
|
703
|
+
* than this are truncated with a head+tail preserving strategy and a notice
|
|
704
|
+
* suffix before being added to the conversation. Prevents one greedy tool
|
|
705
|
+
* call (e.g. a wide log search) from filling the model's context window.
|
|
706
|
+
*
|
|
707
|
+
* Omit or set to 0 to disable.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* tools: {
|
|
711
|
+
* maxOutputBytes: 50_000, // ~12.5K tokens per tool call
|
|
712
|
+
* }
|
|
713
|
+
*/
|
|
714
|
+
maxOutputBytes?: number;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Configuration for automatic compaction of older conversation turns when the
|
|
719
|
+
* estimated input size approaches the model's context window. Prevents
|
|
720
|
+
* upstream 400 "prompt too long" errors without losing the system prompt or
|
|
721
|
+
* the most recent turns.
|
|
722
|
+
*/
|
|
723
|
+
export interface ContextCompactionConfig {
|
|
724
|
+
/**
|
|
725
|
+
* Hard ceiling (in tokens) for the outbound request. Overrides the built-in
|
|
726
|
+
* per-model map. Use this when you know your upstream provider enforces a
|
|
727
|
+
* smaller limit than the model's nominal maximum.
|
|
728
|
+
*/
|
|
729
|
+
maxTokens?: number;
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Fraction of the model ceiling at which compaction kicks in. Defaults to
|
|
733
|
+
* 0.7 — leaves room for the assistant's response and some slack for the
|
|
734
|
+
* chars/4 token heuristic's error.
|
|
735
|
+
*/
|
|
736
|
+
compactAtFraction?: number;
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Number of most-recent messages preserved verbatim during compaction.
|
|
740
|
+
* Defaults to 4 (covers the current turn + its immediate predecessor).
|
|
741
|
+
*/
|
|
742
|
+
keepRecent?: number;
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Disable compaction entirely. Useful in tests and for opting out per-page.
|
|
746
|
+
*/
|
|
747
|
+
disabled?: boolean;
|
|
693
748
|
}
|
|
694
749
|
|
|
695
750
|
export interface WelcomeConfig {
|