@lowdefy/ai-utils 5.3.0 → 5.5.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/AISDKAgentSchema.js +7 -0
- package/dist/buildAgentTools.js +24 -7
- package/dist/handleAgentChat.js +63 -8
- package/package.json +3 -3
package/dist/AISDKAgentSchema.js
CHANGED
|
@@ -213,6 +213,13 @@
|
|
|
213
213
|
type: 'AISDKAgent agent property "repairToolCall" should be a boolean.'
|
|
214
214
|
}
|
|
215
215
|
},
|
|
216
|
+
generateTitle: {
|
|
217
|
+
type: 'boolean',
|
|
218
|
+
description: 'When true, generate a short conversation title on the first turn from the first user message. Emits a "data-chat-title" data part that fires the block\'s onTitleGenerated event. Off by default.',
|
|
219
|
+
errorMessage: {
|
|
220
|
+
type: 'AISDKAgent agent property "generateTitle" should be a boolean.'
|
|
221
|
+
}
|
|
222
|
+
},
|
|
216
223
|
prepareStep: {
|
|
217
224
|
type: 'array',
|
|
218
225
|
items: {
|
package/dist/buildAgentTools.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { createMCPClient } from '@ai-sdk/mcp';
|
|
17
17
|
import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
|
|
18
18
|
import { ConfigError } from '@lowdefy/errors';
|
|
19
|
-
import { serializer } from '@lowdefy/helpers';
|
|
19
|
+
import { serializer, translate } from '@lowdefy/helpers';
|
|
20
20
|
import listFiles from './fileSystem/listFiles.js';
|
|
21
21
|
import readFile from './fileSystem/readFile.js';
|
|
22
22
|
import searchFiles from './fileSystem/searchFiles.js';
|
|
@@ -30,22 +30,36 @@ import RESERVED_PLATFORM_TOOL_NAMES from './reservedToolNames.js';
|
|
|
30
30
|
function cleanBuildArtifact(obj) {
|
|
31
31
|
return JSON.parse(JSON.stringify(serializer.deserialize(obj)));
|
|
32
32
|
}
|
|
33
|
-
function assertNotReserved(name, kind) {
|
|
33
|
+
function assertNotReserved(name, kind, i18n) {
|
|
34
34
|
if (RESERVED_PLATFORM_TOOL_NAMES.includes(name)) {
|
|
35
|
-
throw new ConfigError(
|
|
35
|
+
throw new ConfigError(translate({
|
|
36
|
+
key: 'agent.runtime.reservedToolName',
|
|
37
|
+
values: {
|
|
38
|
+
kind,
|
|
39
|
+
name,
|
|
40
|
+
reserved: RESERVED_PLATFORM_TOOL_NAMES.join(', ')
|
|
41
|
+
},
|
|
42
|
+
i18n
|
|
43
|
+
}));
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
async function buildAgentTools({ agent, context, depth = 0 }) {
|
|
39
47
|
const MAX_DEPTH = 5;
|
|
40
48
|
if (depth > MAX_DEPTH) {
|
|
41
|
-
throw new Error(
|
|
49
|
+
throw new Error(translate({
|
|
50
|
+
key: 'agent.runtime.subAgentDepthExceeded',
|
|
51
|
+
values: {
|
|
52
|
+
max: MAX_DEPTH
|
|
53
|
+
},
|
|
54
|
+
i18n: context.i18n
|
|
55
|
+
}));
|
|
42
56
|
}
|
|
43
57
|
const tools = {};
|
|
44
58
|
const mcpClients = [];
|
|
45
59
|
// Build endpoint tools
|
|
46
60
|
for (const toolConfig of agent.tools ?? []){
|
|
47
61
|
const { endpointId, confirm } = toolConfig;
|
|
48
|
-
assertNotReserved(endpointId, 'Endpoint tool');
|
|
62
|
+
assertNotReserved(endpointId, 'Endpoint tool', context.i18n);
|
|
49
63
|
const endpointConfig = await context.getEndpointConfig({
|
|
50
64
|
endpointId
|
|
51
65
|
});
|
|
@@ -62,7 +76,10 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
|
|
|
62
76
|
});
|
|
63
77
|
if (!result.success) {
|
|
64
78
|
const err = serializer.deserialize(result.error);
|
|
65
|
-
throw new Error(err?.message ??
|
|
79
|
+
throw new Error(err?.message ?? translate({
|
|
80
|
+
key: 'agent.runtime.toolExecutionFailed',
|
|
81
|
+
i18n: context.i18n
|
|
82
|
+
}));
|
|
66
83
|
}
|
|
67
84
|
return cleanBuildArtifact(result.response);
|
|
68
85
|
}
|
|
@@ -135,7 +152,7 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
|
|
|
135
152
|
}
|
|
136
153
|
// Build sub-agent tools
|
|
137
154
|
for (const subAgentRef of agent.agents ?? []){
|
|
138
|
-
assertNotReserved(subAgentRef.agentId, 'Sub-agent');
|
|
155
|
+
assertNotReserved(subAgentRef.agentId, 'Sub-agent', context.i18n);
|
|
139
156
|
const subAgentConfig = await context.getAgentConfig({
|
|
140
157
|
agentId: subAgentRef.agentId
|
|
141
158
|
});
|
package/dist/handleAgentChat.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { ToolLoopAgent, convertToModelMessages, createAgentUIStream, createUIMessageStream, createUIMessageStreamResponse, pruneMessages, stepCountIs, hasToolCall, validateUIMessages } from 'ai';
|
|
15
|
+
*/ import { ToolLoopAgent, convertToModelMessages, createAgentUIStream, createUIMessageStream, createUIMessageStreamResponse, generateText, pruneMessages, stepCountIs, hasToolCall, validateUIMessages } from 'ai';
|
|
16
16
|
import { serializer } from '@lowdefy/helpers';
|
|
17
17
|
import buildAgentTools from './buildAgentTools.js';
|
|
18
18
|
import buildPrepareStep from './buildPrepareStep.js';
|
|
@@ -69,14 +69,17 @@ const hookMapping = {
|
|
|
69
69
|
onToolCallFinish: 'experimental_onToolCallFinish',
|
|
70
70
|
onStepFinish: 'onStepFinish'
|
|
71
71
|
};
|
|
72
|
-
function createHookCallbacks({ hooks, callEndpoint }) {
|
|
72
|
+
function createHookCallbacks({ hooks, callEndpoint, locale }) {
|
|
73
73
|
if (!hooks) return {};
|
|
74
74
|
const callbacks = {};
|
|
75
75
|
for (const [yamlKey, sdkKey] of Object.entries(hookMapping)){
|
|
76
76
|
const endpointIds = hooks[yamlKey];
|
|
77
77
|
if (!endpointIds || endpointIds.length === 0) continue;
|
|
78
78
|
callbacks[sdkKey] = (event)=>{
|
|
79
|
-
const payload =
|
|
79
|
+
const payload = {
|
|
80
|
+
...cleanHookEvent(event),
|
|
81
|
+
locale
|
|
82
|
+
};
|
|
80
83
|
for (const endpointId of endpointIds){
|
|
81
84
|
callEndpoint(endpointId, {
|
|
82
85
|
payload
|
|
@@ -109,6 +112,13 @@ function convertDataUrlsToBase64(messages) {
|
|
|
109
112
|
};
|
|
110
113
|
});
|
|
111
114
|
}
|
|
115
|
+
// Concatenate the text parts of the first user message — used as the source
|
|
116
|
+
// for generateTitle.
|
|
117
|
+
function getFirstUserText(messages) {
|
|
118
|
+
const firstUser = messages.find((msg)=>msg.role === 'user');
|
|
119
|
+
if (!firstUser) return '';
|
|
120
|
+
return (firstUser.parts ?? []).filter((part)=>part.type === 'text').map((part)=>part.text).join('');
|
|
121
|
+
}
|
|
112
122
|
async function handleAgentChat({ connection, properties, context }) {
|
|
113
123
|
const { agent, messages: rawMessages } = properties;
|
|
114
124
|
const messages = convertDataUrlsToBase64(rawMessages);
|
|
@@ -124,9 +134,11 @@ async function handleAgentChat({ connection, properties, context }) {
|
|
|
124
134
|
tools['update-page-state'] = updatePageStateTool;
|
|
125
135
|
}
|
|
126
136
|
const model = connection.provider(agent.properties.model);
|
|
137
|
+
const locale = context.i18n?.active;
|
|
127
138
|
const hookCallbacks = createHookCallbacks({
|
|
128
139
|
hooks: agent.hooks,
|
|
129
|
-
callEndpoint: context.callEndpoint
|
|
140
|
+
callEndpoint: context.callEndpoint,
|
|
141
|
+
locale
|
|
130
142
|
});
|
|
131
143
|
// Prepend page context to instructions when pageContext is enabled
|
|
132
144
|
let instructions = agent.properties.instructions;
|
|
@@ -196,11 +208,16 @@ async function handleAgentChat({ connection, properties, context }) {
|
|
|
196
208
|
const timeoutConfig = agent.properties.timeout != null ? {
|
|
197
209
|
timeout: agent.properties.timeout
|
|
198
210
|
} : {};
|
|
211
|
+
const titleEnabled = agent.properties.generateTitle === true;
|
|
199
212
|
const stream = createUIMessageStream({
|
|
200
213
|
execute: async ({ writer })=>{
|
|
201
214
|
const usageAccumulator = createUsageAccumulator();
|
|
202
215
|
const steps = [];
|
|
203
216
|
let agentStream;
|
|
217
|
+
// The AI SDK delivers the full updated UI message list (input + generated
|
|
218
|
+
// assistant reply) to the stream-level onFinish. Capture it so onFinish
|
|
219
|
+
// hooks persist the complete conversation, not just the request input.
|
|
220
|
+
let capturedMessages;
|
|
204
221
|
function collectStep(stepResult) {
|
|
205
222
|
usageAccumulator.add(stepResult);
|
|
206
223
|
steps.push({
|
|
@@ -211,6 +228,37 @@ async function handleAgentChat({ connection, properties, context }) {
|
|
|
211
228
|
finishReason: stepResult.finishReason
|
|
212
229
|
});
|
|
213
230
|
}
|
|
231
|
+
function captureMessages({ messages: finalMessages }) {
|
|
232
|
+
capturedMessages = finalMessages;
|
|
233
|
+
}
|
|
234
|
+
// Generate a short conversation title from the first user message,
|
|
235
|
+
// concurrently with the agent stream. The data-chat-title part fires the
|
|
236
|
+
// block's onTitleGenerated event. Only on the first turn; failures are
|
|
237
|
+
// non-fatal.
|
|
238
|
+
let titlePromise;
|
|
239
|
+
const isFirstTurn = !messages.some((msg)=>msg.role === 'assistant');
|
|
240
|
+
if (titleEnabled && isFirstTurn) {
|
|
241
|
+
const firstUserText = getFirstUserText(messages);
|
|
242
|
+
if (firstUserText) {
|
|
243
|
+
titlePromise = generateText({
|
|
244
|
+
model,
|
|
245
|
+
maxOutputTokens: 20,
|
|
246
|
+
prompt: `Generate a concise 3-6 word title for a conversation that begins with this message. Reply with only the title, no quotes:\n\n${firstUserText}`
|
|
247
|
+
}).then(({ text })=>{
|
|
248
|
+
const title = text.trim();
|
|
249
|
+
if (title) {
|
|
250
|
+
writer.write({
|
|
251
|
+
type: 'data-chat-title',
|
|
252
|
+
data: {
|
|
253
|
+
title
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}).catch((error)=>{
|
|
258
|
+
console.warn(`generateTitle failed: ${error.message}`);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
214
262
|
if (pruneConfig) {
|
|
215
263
|
// Decompose createAgentUIStream so we can insert pruneMessages
|
|
216
264
|
// between the UIMessage→ModelMessage conversion and agent execution.
|
|
@@ -231,7 +279,8 @@ async function handleAgentChat({ connection, properties, context }) {
|
|
|
231
279
|
onStepFinish: collectStep
|
|
232
280
|
});
|
|
233
281
|
agentStream = result.toUIMessageStream({
|
|
234
|
-
originalMessages: validatedMessages
|
|
282
|
+
originalMessages: validatedMessages,
|
|
283
|
+
onFinish: captureMessages
|
|
235
284
|
});
|
|
236
285
|
} else {
|
|
237
286
|
// createAgentUIStream validates UIMessages, converts to ModelMessages,
|
|
@@ -241,7 +290,8 @@ async function handleAgentChat({ connection, properties, context }) {
|
|
|
241
290
|
agent: agentInstance,
|
|
242
291
|
uiMessages: messages,
|
|
243
292
|
...timeoutConfig,
|
|
244
|
-
onStepFinish: collectStep
|
|
293
|
+
onStepFinish: collectStep,
|
|
294
|
+
onFinish: captureMessages
|
|
245
295
|
});
|
|
246
296
|
}
|
|
247
297
|
// Read the agent stream to completion before running onFinish hooks.
|
|
@@ -260,16 +310,21 @@ async function handleAgentChat({ connection, properties, context }) {
|
|
|
260
310
|
errorText: writer.onError(error)
|
|
261
311
|
});
|
|
262
312
|
}
|
|
313
|
+
// Ensure the title (if any) is written before the stream closes.
|
|
314
|
+
if (titlePromise) {
|
|
315
|
+
await titlePromise;
|
|
316
|
+
}
|
|
263
317
|
// Call onFinish hooks — awaited so dataParts can be written to stream.
|
|
264
318
|
if (hasOnFinishHooks) {
|
|
265
319
|
const finishPayload = {
|
|
266
|
-
messages,
|
|
320
|
+
messages: capturedMessages ?? messages,
|
|
267
321
|
steps,
|
|
268
322
|
toolResults: steps.flatMap((s)=>s.toolResults ?? []),
|
|
269
323
|
finishReason: usageAccumulator.getFinishReason(),
|
|
270
324
|
isAborted: false,
|
|
271
325
|
...context.agentContext ?? {},
|
|
272
|
-
usage: usageAccumulator.usage
|
|
326
|
+
usage: usageAccumulator.usage,
|
|
327
|
+
locale
|
|
273
328
|
};
|
|
274
329
|
for (const endpointId of onFinishEndpointIds){
|
|
275
330
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/ai-utils",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@ai-sdk/mcp": "1.0.41",
|
|
20
|
-
"@lowdefy/errors": "5.
|
|
21
|
-
"@lowdefy/helpers": "5.
|
|
20
|
+
"@lowdefy/errors": "5.5.0",
|
|
21
|
+
"@lowdefy/helpers": "5.5.0",
|
|
22
22
|
"ai": "6.0.176",
|
|
23
23
|
"glob": "13.0.6"
|
|
24
24
|
},
|