@lowdefy/ai-utils 0.0.0-experimental-20260414134741
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/LICENSE +201 -0
- package/dist/AISDKAgent.js +24 -0
- package/dist/AISDKAgentSchema.js +357 -0
- package/dist/agents.js +15 -0
- package/dist/buildAgentTools.js +268 -0
- package/dist/buildPrepareStep.js +42 -0
- package/dist/fileSystem/listFiles.js +62 -0
- package/dist/fileSystem/readFile.js +36 -0
- package/dist/fileSystem/resolvePath.js +26 -0
- package/dist/fileSystem/searchFiles.js +69 -0
- package/dist/fileSystem/statFile.js +30 -0
- package/dist/handleAgentChat.js +268 -0
- package/dist/index.js +15 -0
- package/dist/types.js +19 -0
- package/package.json +35 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { ToolLoopAgent, convertToModelMessages, createAgentUIStream, createUIMessageStream, createUIMessageStreamResponse, pruneMessages, stepCountIs, hasToolCall, validateUIMessages } from 'ai';
|
|
16
|
+
import { serializer } from '@lowdefy/helpers';
|
|
17
|
+
import buildAgentTools from './buildAgentTools.js';
|
|
18
|
+
import buildPrepareStep from './buildPrepareStep.js';
|
|
19
|
+
function createUsageAccumulator() {
|
|
20
|
+
const usage = {
|
|
21
|
+
inputTokens: 0,
|
|
22
|
+
outputTokens: 0,
|
|
23
|
+
totalTokens: 0,
|
|
24
|
+
reasoningTokens: 0,
|
|
25
|
+
cacheReadTokens: 0,
|
|
26
|
+
cacheWriteTokens: 0
|
|
27
|
+
};
|
|
28
|
+
let finishReason = 'stop';
|
|
29
|
+
function add(stepResult) {
|
|
30
|
+
const stepUsage = stepResult?.usage ?? stepResult;
|
|
31
|
+
if (stepResult?.finishReason) {
|
|
32
|
+
finishReason = stepResult.finishReason;
|
|
33
|
+
}
|
|
34
|
+
if (!stepUsage) return;
|
|
35
|
+
usage.inputTokens += stepUsage.inputTokens ?? 0;
|
|
36
|
+
usage.outputTokens += stepUsage.outputTokens ?? 0;
|
|
37
|
+
usage.totalTokens += stepUsage.totalTokens ?? 0;
|
|
38
|
+
usage.reasoningTokens += stepUsage.outputTokenDetails?.reasoningTokens ?? 0;
|
|
39
|
+
usage.cacheReadTokens += stepUsage.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
40
|
+
usage.cacheWriteTokens += stepUsage.inputTokenDetails?.cacheWriteTokens ?? 0;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
usage,
|
|
44
|
+
add,
|
|
45
|
+
getFinishReason: ()=>finishReason
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Strip non-serializable fields from agent-level hook events before sending as payload.
|
|
49
|
+
// messages excluded here — the stream-level onFinish sends UIMessage[] directly.
|
|
50
|
+
function cleanHookEvent(event) {
|
|
51
|
+
const clean = {};
|
|
52
|
+
for (const [key, value] of Object.entries(event)){
|
|
53
|
+
if (key === 'abortSignal') continue;
|
|
54
|
+
if (key === 'messages') continue;
|
|
55
|
+
if (typeof value === 'function') continue;
|
|
56
|
+
clean[key] = value;
|
|
57
|
+
}
|
|
58
|
+
return clean;
|
|
59
|
+
}
|
|
60
|
+
// Maps YAML hook names to AI SDK callback names and creates fire-and-forget callbacks.
|
|
61
|
+
// onFinish is intentionally excluded — it is handled at the stream level inside the
|
|
62
|
+
// createUIMessageStream execute function so that hooks are awaited and can return
|
|
63
|
+
// dataParts to be written to the response stream.
|
|
64
|
+
const hookMapping = {
|
|
65
|
+
onStart: 'experimental_onStart',
|
|
66
|
+
onStepStart: 'experimental_onStepStart',
|
|
67
|
+
onToolCallStart: 'experimental_onToolCallStart',
|
|
68
|
+
onToolCallFinish: 'experimental_onToolCallFinish',
|
|
69
|
+
onStepFinish: 'onStepFinish'
|
|
70
|
+
};
|
|
71
|
+
function createHookCallbacks({ hooks, callEndpoint }) {
|
|
72
|
+
if (!hooks) return {};
|
|
73
|
+
const callbacks = {};
|
|
74
|
+
for (const [yamlKey, sdkKey] of Object.entries(hookMapping)){
|
|
75
|
+
const endpointIds = hooks[yamlKey];
|
|
76
|
+
if (!endpointIds || endpointIds.length === 0) continue;
|
|
77
|
+
callbacks[sdkKey] = (event)=>{
|
|
78
|
+
const payload = cleanHookEvent(event);
|
|
79
|
+
for (const endpointId of endpointIds){
|
|
80
|
+
callEndpoint(endpointId, {
|
|
81
|
+
payload
|
|
82
|
+
}).catch(()=>{});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return callbacks;
|
|
87
|
+
}
|
|
88
|
+
// Convert data: URLs in file parts to raw base64 so the AI SDK does not attempt
|
|
89
|
+
// to download them (it only supports http/https). The mediaType field already
|
|
90
|
+
// carries the MIME type, so nothing is lost.
|
|
91
|
+
function convertDataUrlsToBase64(messages) {
|
|
92
|
+
return messages.map((msg)=>{
|
|
93
|
+
if (!msg.parts) return msg;
|
|
94
|
+
const converted = msg.parts.map((part)=>{
|
|
95
|
+
if (part.type !== 'file' || typeof part.url !== 'string' || !part.url.startsWith('data:')) {
|
|
96
|
+
return part;
|
|
97
|
+
}
|
|
98
|
+
const commaIndex = part.url.indexOf(',');
|
|
99
|
+
if (commaIndex === -1) return part;
|
|
100
|
+
return {
|
|
101
|
+
...part,
|
|
102
|
+
url: part.url.slice(commaIndex + 1)
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
...msg,
|
|
107
|
+
parts: converted
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function handleAgentChat({ connection, properties, context }) {
|
|
112
|
+
const { agent, messages: rawMessages } = properties;
|
|
113
|
+
const messages = convertDataUrlsToBase64(rawMessages);
|
|
114
|
+
const { tools, mcpClients } = await buildAgentTools({
|
|
115
|
+
agent,
|
|
116
|
+
context
|
|
117
|
+
});
|
|
118
|
+
const model = connection.provider(agent.properties.model);
|
|
119
|
+
const hookCallbacks = createHookCallbacks({
|
|
120
|
+
hooks: agent.hooks,
|
|
121
|
+
callEndpoint: context.callEndpoint
|
|
122
|
+
});
|
|
123
|
+
// Prepend page context to instructions when pageContext is enabled
|
|
124
|
+
let instructions = agent.properties.instructions;
|
|
125
|
+
if (agent.properties.pageContext && context.agentContext) {
|
|
126
|
+
const ctx = context.agentContext;
|
|
127
|
+
const contextLines = [
|
|
128
|
+
'<context>'
|
|
129
|
+
];
|
|
130
|
+
if (ctx.pageId) contextLines.push(` pageId: ${ctx.pageId}`);
|
|
131
|
+
if (ctx.userId) contextLines.push(` userId: ${ctx.userId}`);
|
|
132
|
+
if (ctx.conversationId) contextLines.push(` conversationId: ${ctx.conversationId}`);
|
|
133
|
+
if (ctx.urlQuery && Object.keys(ctx.urlQuery).length > 0) {
|
|
134
|
+
contextLines.push(` urlQuery: ${JSON.stringify(ctx.urlQuery)}`);
|
|
135
|
+
}
|
|
136
|
+
contextLines.push('</context>');
|
|
137
|
+
instructions = `${contextLines.join('\n')}\n\n${instructions ?? ''}`;
|
|
138
|
+
}
|
|
139
|
+
// Build stop conditions
|
|
140
|
+
const stopConditions = [
|
|
141
|
+
stepCountIs(agent.properties.maxSteps ?? 10)
|
|
142
|
+
];
|
|
143
|
+
const stopOnToolCall = agent.properties.stopOnToolCall;
|
|
144
|
+
if (stopOnToolCall) {
|
|
145
|
+
const toolNames = Array.isArray(stopOnToolCall) ? stopOnToolCall : [
|
|
146
|
+
stopOnToolCall
|
|
147
|
+
];
|
|
148
|
+
for (const name of toolNames){
|
|
149
|
+
stopConditions.push(hasToolCall(name));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const agentInstance = new ToolLoopAgent({
|
|
153
|
+
model,
|
|
154
|
+
instructions,
|
|
155
|
+
tools,
|
|
156
|
+
stopWhen: stopConditions.length === 1 ? stopConditions[0] : stopConditions,
|
|
157
|
+
maxOutputTokens: agent.properties.maxOutputTokens,
|
|
158
|
+
temperature: agent.properties.temperature,
|
|
159
|
+
toolChoice: agent.properties.toolChoice ?? 'auto',
|
|
160
|
+
providerOptions: agent.properties.providerOptions,
|
|
161
|
+
activeTools: agent.properties.activeTools,
|
|
162
|
+
topP: agent.properties.topP,
|
|
163
|
+
topK: agent.properties.topK,
|
|
164
|
+
frequencyPenalty: agent.properties.frequencyPenalty,
|
|
165
|
+
presencePenalty: agent.properties.presencePenalty,
|
|
166
|
+
seed: agent.properties.seed,
|
|
167
|
+
stopSequences: agent.properties.stopSequences,
|
|
168
|
+
maxRetries: agent.properties.maxRetries,
|
|
169
|
+
...agent.properties.prepareStep ? {
|
|
170
|
+
prepareStep: buildPrepareStep(agent.properties.prepareStep)
|
|
171
|
+
} : {},
|
|
172
|
+
...hookCallbacks,
|
|
173
|
+
...agent.properties.repairToolCall ? {
|
|
174
|
+
experimental_repairToolCall: async ({ toolCall })=>{
|
|
175
|
+
return {
|
|
176
|
+
...toolCall
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
} : {}
|
|
180
|
+
});
|
|
181
|
+
const onFinishEndpointIds = agent.hooks?.onFinish;
|
|
182
|
+
const hasOnFinishHooks = onFinishEndpointIds && onFinishEndpointIds.length > 0;
|
|
183
|
+
const hasMcpClients = mcpClients.length > 0;
|
|
184
|
+
const pruneConfig = agent.properties.prune;
|
|
185
|
+
const timeoutConfig = agent.properties.timeout != null ? {
|
|
186
|
+
timeout: agent.properties.timeout
|
|
187
|
+
} : {};
|
|
188
|
+
const stream = createUIMessageStream({
|
|
189
|
+
execute: async ({ writer })=>{
|
|
190
|
+
const usageAccumulator = createUsageAccumulator();
|
|
191
|
+
let agentStream;
|
|
192
|
+
if (pruneConfig) {
|
|
193
|
+
// Decompose createAgentUIStream so we can insert pruneMessages
|
|
194
|
+
// between the UIMessage→ModelMessage conversion and agent execution.
|
|
195
|
+
const validatedMessages = await validateUIMessages({
|
|
196
|
+
messages,
|
|
197
|
+
tools: agentInstance.tools
|
|
198
|
+
});
|
|
199
|
+
const modelMessages = await convertToModelMessages(validatedMessages, {
|
|
200
|
+
tools: agentInstance.tools
|
|
201
|
+
});
|
|
202
|
+
const prunedMessages = pruneMessages({
|
|
203
|
+
messages: modelMessages,
|
|
204
|
+
...pruneConfig
|
|
205
|
+
});
|
|
206
|
+
const result = await agentInstance.stream({
|
|
207
|
+
prompt: prunedMessages,
|
|
208
|
+
...timeoutConfig,
|
|
209
|
+
onStepFinish: (stepResult)=>{
|
|
210
|
+
usageAccumulator.add(stepResult);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
agentStream = result.toUIMessageStream({
|
|
214
|
+
originalMessages: validatedMessages
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
// createAgentUIStream validates UIMessages, converts to ModelMessages,
|
|
218
|
+
// runs the agent, and returns a UIMessageStream — handling the full
|
|
219
|
+
// UI→model→UI conversion that ToolLoopAgent.stream() does not.
|
|
220
|
+
agentStream = await createAgentUIStream({
|
|
221
|
+
agent: agentInstance,
|
|
222
|
+
uiMessages: messages,
|
|
223
|
+
...timeoutConfig,
|
|
224
|
+
onStepFinish: (stepResult)=>{
|
|
225
|
+
usageAccumulator.add(stepResult);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
writer.merge(agentStream);
|
|
230
|
+
// After merge resolves the agent stream is complete.
|
|
231
|
+
// Call onFinish hooks — awaited so dataParts can be written to stream.
|
|
232
|
+
if (hasOnFinishHooks) {
|
|
233
|
+
const finishPayload = {
|
|
234
|
+
messages,
|
|
235
|
+
finishReason: usageAccumulator.getFinishReason(),
|
|
236
|
+
isAborted: false,
|
|
237
|
+
...context.agentContext ?? {},
|
|
238
|
+
usage: usageAccumulator.usage
|
|
239
|
+
};
|
|
240
|
+
for (const endpointId of onFinishEndpointIds){
|
|
241
|
+
try {
|
|
242
|
+
const hookResponse = await context.callEndpoint(endpointId, {
|
|
243
|
+
payload: finishPayload
|
|
244
|
+
});
|
|
245
|
+
const responseData = serializer.deserialize(hookResponse?.response);
|
|
246
|
+
if (Array.isArray(responseData?.dataParts)) {
|
|
247
|
+
for (const part of responseData.dataParts){
|
|
248
|
+
writer.write(part);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.warn(`onFinish hook "${endpointId}" failed: ${error.message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (hasMcpClients) {
|
|
257
|
+
await Promise.all(mcpClients.map(({ client })=>client.close().catch(()=>{})));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
const response = createUIMessageStreamResponse({
|
|
262
|
+
stream
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
response
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
export default handleAgentChat;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ export { default as handleAgentChat } from './handleAgentChat.js';
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ export default {
|
|
16
|
+
agents: [
|
|
17
|
+
'AISDKAgent'
|
|
18
|
+
]
|
|
19
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lowdefy/ai-utils",
|
|
3
|
+
"version": "0.0.0-experimental-20260414134741",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js",
|
|
8
|
+
"./agents": "./dist/agents.js",
|
|
9
|
+
"./types": "./dist/types.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/*"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@ai-sdk/mcp": "1.0.30",
|
|
16
|
+
"ai": "6.0.116",
|
|
17
|
+
"glob": "11.0.2",
|
|
18
|
+
"@lowdefy/helpers": "0.0.0-experimental-20260414134741"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@jest/globals": "28.1.3",
|
|
22
|
+
"@swc/cli": "0.8.0",
|
|
23
|
+
"@swc/core": "1.15.18",
|
|
24
|
+
"@swc/jest": "0.2.39",
|
|
25
|
+
"jest": "28.1.3"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "swc src --out-dir dist --config-file ../../../.swcrc --cli-config-file ../../../.swc-cli.json",
|
|
32
|
+
"clean": "rm -rf dist",
|
|
33
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
34
|
+
}
|
|
35
|
+
}
|