@lowdefy/ai-utils 5.6.0 → 6.0.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/GenerateObjectSchema.js +155 -0
- package/dist/GenerateTextSchema.js +142 -0
- package/dist/buildAgentTools.js +23 -14
- package/dist/buildGenerateCallOptions.js +50 -0
- package/dist/createGenerateObject.js +38 -0
- package/dist/createGenerateText.js +38 -0
- package/dist/createToolLoopAgent.js +157 -0
- package/dist/createUsageAccumulator.js +44 -0
- package/dist/handleAgentChat.js +99 -195
- package/dist/handleAgentGenerate.js +93 -0
- package/dist/handleGenerateObject.js +40 -0
- package/dist/handleGenerateText.js +33 -0
- package/dist/index.js +4 -1
- package/package.json +6 -5
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
17
|
+
title: 'Lowdefy Request Schema - GenerateObject',
|
|
18
|
+
type: 'object',
|
|
19
|
+
required: [
|
|
20
|
+
'model',
|
|
21
|
+
'schema'
|
|
22
|
+
],
|
|
23
|
+
properties: {
|
|
24
|
+
model: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Model id to generate with.',
|
|
27
|
+
errorMessage: {
|
|
28
|
+
type: 'GenerateObject request property "model" should be a string.'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
schema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
description: 'JSON Schema describing the object the model must generate.',
|
|
34
|
+
errorMessage: {
|
|
35
|
+
type: 'GenerateObject request property "schema" should be an object.'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
schemaName: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Optional name for the output schema, passed to the model.',
|
|
41
|
+
errorMessage: {
|
|
42
|
+
type: 'GenerateObject request property "schemaName" should be a string.'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
schemaDescription: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Optional description of the output schema, passed to the model.',
|
|
48
|
+
errorMessage: {
|
|
49
|
+
type: 'GenerateObject request property "schemaDescription" should be a string.'
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
prompt: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Text prompt. Use either "prompt" or "messages", not both.',
|
|
55
|
+
errorMessage: {
|
|
56
|
+
type: 'GenerateObject request property "prompt" should be a string.'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
messages: {
|
|
60
|
+
type: 'array',
|
|
61
|
+
items: {
|
|
62
|
+
type: 'object'
|
|
63
|
+
},
|
|
64
|
+
description: 'Model messages ({ role, content }). Use either "prompt" or "messages", not both.',
|
|
65
|
+
errorMessage: {
|
|
66
|
+
type: 'GenerateObject request property "messages" should be an array of objects.'
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
system: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'System prompt.',
|
|
72
|
+
errorMessage: {
|
|
73
|
+
type: 'GenerateObject request property "system" should be a string.'
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
maxOutputTokens: {
|
|
77
|
+
type: 'integer',
|
|
78
|
+
minimum: 1,
|
|
79
|
+
description: 'Maximum number of tokens to generate.',
|
|
80
|
+
errorMessage: {
|
|
81
|
+
type: 'GenerateObject request property "maxOutputTokens" should be an integer.',
|
|
82
|
+
minimum: 'GenerateObject request property "maxOutputTokens" should be at least 1.'
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
temperature: {
|
|
86
|
+
type: 'number',
|
|
87
|
+
minimum: 0,
|
|
88
|
+
maximum: 2,
|
|
89
|
+
description: 'Sampling temperature.',
|
|
90
|
+
errorMessage: {
|
|
91
|
+
type: 'GenerateObject request property "temperature" should be a number.',
|
|
92
|
+
minimum: 'GenerateObject request property "temperature" should be at least 0.',
|
|
93
|
+
maximum: 'GenerateObject request property "temperature" should be at most 2.'
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
topP: {
|
|
97
|
+
type: 'number',
|
|
98
|
+
description: 'Nucleus sampling.',
|
|
99
|
+
errorMessage: {
|
|
100
|
+
type: 'GenerateObject request property "topP" should be a number.'
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
topK: {
|
|
104
|
+
type: 'number',
|
|
105
|
+
description: 'Only sample from the top K options for each subsequent token.',
|
|
106
|
+
errorMessage: {
|
|
107
|
+
type: 'GenerateObject request property "topK" should be a number.'
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
frequencyPenalty: {
|
|
111
|
+
type: 'number',
|
|
112
|
+
description: 'Penalize repeated tokens by frequency.',
|
|
113
|
+
errorMessage: {
|
|
114
|
+
type: 'GenerateObject request property "frequencyPenalty" should be a number.'
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
presencePenalty: {
|
|
118
|
+
type: 'number',
|
|
119
|
+
description: 'Penalize tokens that have already appeared.',
|
|
120
|
+
errorMessage: {
|
|
121
|
+
type: 'GenerateObject request property "presencePenalty" should be a number.'
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
seed: {
|
|
125
|
+
type: 'integer',
|
|
126
|
+
description: 'Seed for deterministic sampling, if supported by the model.',
|
|
127
|
+
errorMessage: {
|
|
128
|
+
type: 'GenerateObject request property "seed" should be an integer.'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
maxRetries: {
|
|
132
|
+
type: 'integer',
|
|
133
|
+
minimum: 0,
|
|
134
|
+
description: 'Maximum number of retries. Defaults to 2.',
|
|
135
|
+
errorMessage: {
|
|
136
|
+
type: 'GenerateObject request property "maxRetries" should be an integer.',
|
|
137
|
+
minimum: 'GenerateObject request property "maxRetries" should be at least 0.'
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
providerOptions: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
description: 'Provider-specific options, keyed by provider.',
|
|
143
|
+
errorMessage: {
|
|
144
|
+
type: 'GenerateObject request property "providerOptions" should be an object.'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
errorMessage: {
|
|
149
|
+
type: 'GenerateObject request properties should be an object.',
|
|
150
|
+
required: {
|
|
151
|
+
model: 'GenerateObject request should have required property "model".',
|
|
152
|
+
schema: 'GenerateObject request should have required property "schema".'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
17
|
+
title: 'Lowdefy Request Schema - GenerateText',
|
|
18
|
+
type: 'object',
|
|
19
|
+
required: [
|
|
20
|
+
'model'
|
|
21
|
+
],
|
|
22
|
+
properties: {
|
|
23
|
+
model: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Model id to generate with.',
|
|
26
|
+
errorMessage: {
|
|
27
|
+
type: 'GenerateText request property "model" should be a string.'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
prompt: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Text prompt. Use either "prompt" or "messages", not both.',
|
|
33
|
+
errorMessage: {
|
|
34
|
+
type: 'GenerateText request property "prompt" should be a string.'
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
messages: {
|
|
38
|
+
type: 'array',
|
|
39
|
+
items: {
|
|
40
|
+
type: 'object'
|
|
41
|
+
},
|
|
42
|
+
description: 'Model messages ({ role, content }). Use either "prompt" or "messages", not both.',
|
|
43
|
+
errorMessage: {
|
|
44
|
+
type: 'GenerateText request property "messages" should be an array of objects.'
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
system: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'System prompt.',
|
|
50
|
+
errorMessage: {
|
|
51
|
+
type: 'GenerateText request property "system" should be a string.'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
maxOutputTokens: {
|
|
55
|
+
type: 'integer',
|
|
56
|
+
minimum: 1,
|
|
57
|
+
description: 'Maximum number of tokens to generate.',
|
|
58
|
+
errorMessage: {
|
|
59
|
+
type: 'GenerateText request property "maxOutputTokens" should be an integer.',
|
|
60
|
+
minimum: 'GenerateText request property "maxOutputTokens" should be at least 1.'
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
temperature: {
|
|
64
|
+
type: 'number',
|
|
65
|
+
minimum: 0,
|
|
66
|
+
maximum: 2,
|
|
67
|
+
description: 'Sampling temperature.',
|
|
68
|
+
errorMessage: {
|
|
69
|
+
type: 'GenerateText request property "temperature" should be a number.',
|
|
70
|
+
minimum: 'GenerateText request property "temperature" should be at least 0.',
|
|
71
|
+
maximum: 'GenerateText request property "temperature" should be at most 2.'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
topP: {
|
|
75
|
+
type: 'number',
|
|
76
|
+
description: 'Nucleus sampling.',
|
|
77
|
+
errorMessage: {
|
|
78
|
+
type: 'GenerateText request property "topP" should be a number.'
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
topK: {
|
|
82
|
+
type: 'number',
|
|
83
|
+
description: 'Only sample from the top K options for each subsequent token.',
|
|
84
|
+
errorMessage: {
|
|
85
|
+
type: 'GenerateText request property "topK" should be a number.'
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
frequencyPenalty: {
|
|
89
|
+
type: 'number',
|
|
90
|
+
description: 'Penalize repeated tokens by frequency.',
|
|
91
|
+
errorMessage: {
|
|
92
|
+
type: 'GenerateText request property "frequencyPenalty" should be a number.'
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
presencePenalty: {
|
|
96
|
+
type: 'number',
|
|
97
|
+
description: 'Penalize tokens that have already appeared.',
|
|
98
|
+
errorMessage: {
|
|
99
|
+
type: 'GenerateText request property "presencePenalty" should be a number.'
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
seed: {
|
|
103
|
+
type: 'integer',
|
|
104
|
+
description: 'Seed for deterministic sampling, if supported by the model.',
|
|
105
|
+
errorMessage: {
|
|
106
|
+
type: 'GenerateText request property "seed" should be an integer.'
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
stopSequences: {
|
|
110
|
+
type: 'array',
|
|
111
|
+
items: {
|
|
112
|
+
type: 'string'
|
|
113
|
+
},
|
|
114
|
+
description: 'Sequences that stop generation.',
|
|
115
|
+
errorMessage: {
|
|
116
|
+
type: 'GenerateText request property "stopSequences" should be an array of strings.'
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
maxRetries: {
|
|
120
|
+
type: 'integer',
|
|
121
|
+
minimum: 0,
|
|
122
|
+
description: 'Maximum number of retries. Defaults to 2.',
|
|
123
|
+
errorMessage: {
|
|
124
|
+
type: 'GenerateText request property "maxRetries" should be an integer.',
|
|
125
|
+
minimum: 'GenerateText request property "maxRetries" should be at least 0.'
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
providerOptions: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
description: 'Provider-specific options, keyed by provider.',
|
|
131
|
+
errorMessage: {
|
|
132
|
+
type: 'GenerateText request property "providerOptions" should be an object.'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
errorMessage: {
|
|
137
|
+
type: 'GenerateText request properties should be an object.',
|
|
138
|
+
required: {
|
|
139
|
+
model: 'GenerateText request should have required property "model".'
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
package/dist/buildAgentTools.js
CHANGED
|
@@ -43,7 +43,9 @@ function assertNotPlatformToolName(name, kind, i18n) {
|
|
|
43
43
|
}));
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
// autoApprove skips needsApproval on confirm tools — used by headless
|
|
47
|
+
// run-to-completion agents, where no client exists to resolve an approval.
|
|
48
|
+
async function buildAgentTools({ agent, context, depth = 0, autoApprove = false }) {
|
|
47
49
|
const MAX_DEPTH = 5;
|
|
48
50
|
if (depth > MAX_DEPTH) {
|
|
49
51
|
throw new Error(translate({
|
|
@@ -56,26 +58,29 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
|
|
|
56
58
|
}
|
|
57
59
|
const tools = {};
|
|
58
60
|
const mcpClients = [];
|
|
59
|
-
// Build endpoint tools
|
|
61
|
+
// Build endpoint tools. The model-facing tool name is toolConfig.name
|
|
62
|
+
// (assigned by buildAgents — endpoint id with '/' → '__' unless overridden);
|
|
63
|
+
// execution still targets the endpointId.
|
|
60
64
|
for (const toolConfig of agent.tools ?? []){
|
|
61
65
|
const { endpointId, confirm } = toolConfig;
|
|
62
|
-
|
|
66
|
+
const toolName = toolConfig.name ?? endpointId;
|
|
67
|
+
assertNotPlatformToolName(toolName, 'Endpoint tool', context.i18n);
|
|
63
68
|
// A second, disjoint list: RESERVED_PLATFORM_TOOL_NAMES are the tools Lowdefy itself registers,
|
|
64
69
|
// isReserved are the prototype-pollution keys setKey refuses. A name can pass one gate and fail
|
|
65
70
|
// the other, so both checks are needed. The build rejects reserved key names, so this only fires
|
|
66
71
|
// for a stale or hand-edited build artifact. Skip the tool rather than fail the whole agent, as
|
|
67
72
|
// the MCP name cases below do.
|
|
68
|
-
if (isReserved(
|
|
69
|
-
console.warn(`Endpoint tool "${
|
|
73
|
+
if (isReserved(toolName)) {
|
|
74
|
+
console.warn(`Endpoint tool "${toolName}" uses a reserved key name — skipped.`);
|
|
70
75
|
continue;
|
|
71
76
|
}
|
|
72
77
|
const endpointConfig = await context.getEndpointConfig({
|
|
73
78
|
endpointId
|
|
74
79
|
});
|
|
75
|
-
setKey(tools,
|
|
80
|
+
setKey(tools, toolName, tool({
|
|
76
81
|
description: endpointConfig.description,
|
|
77
82
|
inputSchema: jsonSchema(cleanBuildArtifact(endpointConfig.payloadSchema)),
|
|
78
|
-
...confirm ? {
|
|
83
|
+
...confirm && !autoApprove ? {
|
|
79
84
|
needsApproval: true
|
|
80
85
|
} : {},
|
|
81
86
|
execute: async (input, { abortSignal } = {})=>{
|
|
@@ -151,7 +156,8 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
|
|
|
151
156
|
console.warn(`MCP tool "${name}" from ${source.url ?? source.command} ` + `conflicts with endpoint tool — skipped.`);
|
|
152
157
|
continue;
|
|
153
158
|
}
|
|
154
|
-
|
|
159
|
+
const needsApproval = source.confirm && !autoApprove;
|
|
160
|
+
setKey(tools, name, needsApproval ? {
|
|
155
161
|
...mcpTool,
|
|
156
162
|
needsApproval: true
|
|
157
163
|
} : mcpTool);
|
|
@@ -161,13 +167,15 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
|
|
|
161
167
|
console.warn(`MCP server "${label}" tool listing failed: ${err.message}`);
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
|
-
// Build sub-agent tools
|
|
170
|
+
// Build sub-agent tools — keyed by name for the same provider-rule reason
|
|
171
|
+
// as endpoint tools (scoped agent ids contain '/').
|
|
165
172
|
for (const subAgentRef of agent.agents ?? []){
|
|
166
|
-
|
|
173
|
+
const subAgentToolName = subAgentRef.name ?? subAgentRef.agentId;
|
|
174
|
+
assertNotPlatformToolName(subAgentToolName, 'Sub-agent', context.i18n);
|
|
167
175
|
// Same policy as the endpoint tool name above: unreachable from valid config, so skip rather
|
|
168
176
|
// than fail the whole agent.
|
|
169
|
-
if (isReserved(
|
|
170
|
-
console.warn(`Sub-agent tool "${
|
|
177
|
+
if (isReserved(subAgentToolName)) {
|
|
178
|
+
console.warn(`Sub-agent tool "${subAgentToolName}" uses a reserved key name — skipped.`);
|
|
171
179
|
continue;
|
|
172
180
|
}
|
|
173
181
|
const subAgentConfig = await context.getAgentConfig({
|
|
@@ -183,7 +191,8 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
|
|
|
183
191
|
const { tools: subTools, mcpClients: subMcpClients } = await buildAgentTools({
|
|
184
192
|
agent: subAgentConfig,
|
|
185
193
|
context,
|
|
186
|
-
depth: depth + 1
|
|
194
|
+
depth: depth + 1,
|
|
195
|
+
autoApprove
|
|
187
196
|
});
|
|
188
197
|
const subModel = subConnection.provider(subAgentConfig.properties.model);
|
|
189
198
|
const subAgent = new ToolLoopAgent({
|
|
@@ -209,7 +218,7 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
|
|
|
209
218
|
'task'
|
|
210
219
|
]
|
|
211
220
|
});
|
|
212
|
-
setKey(tools,
|
|
221
|
+
setKey(tools, subAgentToolName, tool({
|
|
213
222
|
description,
|
|
214
223
|
inputSchema,
|
|
215
224
|
execute: async (input, { abortSignal })=>{
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
16
|
+
const callSettings = [
|
|
17
|
+
'system',
|
|
18
|
+
'maxOutputTokens',
|
|
19
|
+
'temperature',
|
|
20
|
+
'topP',
|
|
21
|
+
'topK',
|
|
22
|
+
'frequencyPenalty',
|
|
23
|
+
'presencePenalty',
|
|
24
|
+
'seed',
|
|
25
|
+
'stopSequences',
|
|
26
|
+
'maxRetries',
|
|
27
|
+
'providerOptions'
|
|
28
|
+
];
|
|
29
|
+
function buildGenerateCallOptions({ request }) {
|
|
30
|
+
if (type.isNone(request.prompt) && type.isNone(request.messages)) {
|
|
31
|
+
throw new Error('Either "prompt" or "messages" must be provided.');
|
|
32
|
+
}
|
|
33
|
+
if (!type.isNone(request.prompt) && !type.isNone(request.messages)) {
|
|
34
|
+
throw new Error('Only one of "prompt" or "messages" may be provided, not both.');
|
|
35
|
+
}
|
|
36
|
+
const options = {};
|
|
37
|
+
if (!type.isNone(request.prompt)) {
|
|
38
|
+
options.prompt = request.prompt;
|
|
39
|
+
}
|
|
40
|
+
if (!type.isNone(request.messages)) {
|
|
41
|
+
options.messages = request.messages;
|
|
42
|
+
}
|
|
43
|
+
callSettings.forEach((setting)=>{
|
|
44
|
+
if (!type.isNone(request[setting])) {
|
|
45
|
+
options[setting] = request[setting];
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return options;
|
|
49
|
+
}
|
|
50
|
+
export default buildGenerateCallOptions;
|
|
@@ -0,0 +1,38 @@
|
|
|
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 handleGenerateObject from './handleGenerateObject.js';
|
|
16
|
+
import schema from './GenerateObjectSchema.js';
|
|
17
|
+
// Builds the GenerateObject request resolver for an AI provider connection.
|
|
18
|
+
// createProvider is the only provider-specific piece — request resolvers
|
|
19
|
+
// receive evaluated connection properties (not a provider instance) from the
|
|
20
|
+
// request interface layer, so the resolver constructs the provider itself.
|
|
21
|
+
function createGenerateObject({ createProvider }) {
|
|
22
|
+
async function GenerateObject({ connection, request }) {
|
|
23
|
+
const provider = createProvider({
|
|
24
|
+
connection
|
|
25
|
+
});
|
|
26
|
+
return handleGenerateObject({
|
|
27
|
+
model: provider(request.model),
|
|
28
|
+
request
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
GenerateObject.schema = schema;
|
|
32
|
+
GenerateObject.meta = {
|
|
33
|
+
checkRead: false,
|
|
34
|
+
checkWrite: false
|
|
35
|
+
};
|
|
36
|
+
return GenerateObject;
|
|
37
|
+
}
|
|
38
|
+
export default createGenerateObject;
|
|
@@ -0,0 +1,38 @@
|
|
|
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 handleGenerateText from './handleGenerateText.js';
|
|
16
|
+
import schema from './GenerateTextSchema.js';
|
|
17
|
+
// Builds the GenerateText request resolver for an AI provider connection.
|
|
18
|
+
// createProvider is the only provider-specific piece — request resolvers
|
|
19
|
+
// receive evaluated connection properties (not a provider instance) from the
|
|
20
|
+
// request interface layer, so the resolver constructs the provider itself.
|
|
21
|
+
function createGenerateText({ createProvider }) {
|
|
22
|
+
async function GenerateText({ connection, request }) {
|
|
23
|
+
const provider = createProvider({
|
|
24
|
+
connection
|
|
25
|
+
});
|
|
26
|
+
return handleGenerateText({
|
|
27
|
+
model: provider(request.model),
|
|
28
|
+
request
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
GenerateText.schema = schema;
|
|
32
|
+
GenerateText.meta = {
|
|
33
|
+
checkRead: false,
|
|
34
|
+
checkWrite: false
|
|
35
|
+
};
|
|
36
|
+
return GenerateText;
|
|
37
|
+
}
|
|
38
|
+
export default createGenerateText;
|
|
@@ -0,0 +1,157 @@
|
|
|
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, stepCountIs, hasToolCall } from 'ai';
|
|
16
|
+
import buildAgentTools from './buildAgentTools.js';
|
|
17
|
+
import buildPrepareStep from './buildPrepareStep.js';
|
|
18
|
+
import buildUpdatePageStateTool from './buildUpdatePageStateTool.js';
|
|
19
|
+
// Strip non-serializable fields from agent-level hook events before sending as payload.
|
|
20
|
+
// messages excluded here — the stream-level onFinish sends UIMessage[] directly.
|
|
21
|
+
function cleanHookEvent(event) {
|
|
22
|
+
const clean = {};
|
|
23
|
+
for (const [key, value] of Object.entries(event)){
|
|
24
|
+
if (key === 'abortSignal') continue;
|
|
25
|
+
if (key === 'messages') continue;
|
|
26
|
+
if (typeof value === 'function') continue;
|
|
27
|
+
clean[key] = value;
|
|
28
|
+
}
|
|
29
|
+
return clean;
|
|
30
|
+
}
|
|
31
|
+
// Maps YAML hook names to AI SDK callback names and creates fire-and-forget callbacks.
|
|
32
|
+
// onFinish is intentionally excluded — it is handled by the callers (at the stream
|
|
33
|
+
// level in handleAgentChat, after generate in handleAgentGenerate) so that hooks
|
|
34
|
+
// are awaited.
|
|
35
|
+
const hookMapping = {
|
|
36
|
+
onStart: 'experimental_onStart',
|
|
37
|
+
onStepStart: 'experimental_onStepStart',
|
|
38
|
+
onToolCallStart: 'experimental_onToolCallStart',
|
|
39
|
+
onToolCallFinish: 'experimental_onToolCallFinish',
|
|
40
|
+
onStepFinish: 'onStepFinish'
|
|
41
|
+
};
|
|
42
|
+
function createHookCallbacks({ hooks, callEndpoint, locale }) {
|
|
43
|
+
if (!hooks) return {};
|
|
44
|
+
const callbacks = {};
|
|
45
|
+
for (const [yamlKey, sdkKey] of Object.entries(hookMapping)){
|
|
46
|
+
const endpointIds = hooks[yamlKey];
|
|
47
|
+
if (!endpointIds || endpointIds.length === 0) continue;
|
|
48
|
+
callbacks[sdkKey] = (event)=>{
|
|
49
|
+
const payload = {
|
|
50
|
+
...cleanHookEvent(event),
|
|
51
|
+
locale
|
|
52
|
+
};
|
|
53
|
+
for (const endpointId of endpointIds){
|
|
54
|
+
callEndpoint(endpointId, {
|
|
55
|
+
payload
|
|
56
|
+
}).catch(()=>{});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return callbacks;
|
|
61
|
+
}
|
|
62
|
+
// Shared agent assembly for the streaming chat path and the headless generate
|
|
63
|
+
// path: tools, model, hook callbacks, instructions, stop conditions, and the
|
|
64
|
+
// ToolLoopAgent instance.
|
|
65
|
+
async function createToolLoopAgent({ connection, agent, context, autoApprove = false }) {
|
|
66
|
+
const { tools, mcpClients } = await buildAgentTools({
|
|
67
|
+
agent,
|
|
68
|
+
context,
|
|
69
|
+
autoApprove
|
|
70
|
+
});
|
|
71
|
+
const sharedState = context.agentContext?.sharedState;
|
|
72
|
+
const updatePageStateTool = buildUpdatePageStateTool({
|
|
73
|
+
sharedState
|
|
74
|
+
});
|
|
75
|
+
if (updatePageStateTool) {
|
|
76
|
+
tools['update-page-state'] = updatePageStateTool;
|
|
77
|
+
}
|
|
78
|
+
const model = connection.provider(agent.properties.model);
|
|
79
|
+
const locale = context.i18n?.active;
|
|
80
|
+
const hookCallbacks = createHookCallbacks({
|
|
81
|
+
hooks: agent.hooks,
|
|
82
|
+
callEndpoint: context.callEndpoint,
|
|
83
|
+
locale
|
|
84
|
+
});
|
|
85
|
+
// Prepend page context to instructions when pageContext is enabled
|
|
86
|
+
let instructions = agent.properties.instructions;
|
|
87
|
+
if (agent.properties.pageContext && context.agentContext) {
|
|
88
|
+
const ctx = context.agentContext;
|
|
89
|
+
const contextLines = [
|
|
90
|
+
'<context>'
|
|
91
|
+
];
|
|
92
|
+
if (ctx.pageId) contextLines.push(` pageId: ${ctx.pageId}`);
|
|
93
|
+
if (ctx.userId) contextLines.push(` userId: ${ctx.userId}`);
|
|
94
|
+
if (ctx.conversationId) contextLines.push(` conversationId: ${ctx.conversationId}`);
|
|
95
|
+
if (ctx.urlQuery && Object.keys(ctx.urlQuery).length > 0) {
|
|
96
|
+
contextLines.push(` urlQuery: ${JSON.stringify(ctx.urlQuery)}`);
|
|
97
|
+
}
|
|
98
|
+
if (ctx.sharedState && Object.keys(ctx.sharedState).length > 0) {
|
|
99
|
+
contextLines.push(` sharedState: ${JSON.stringify(ctx.sharedState)}`);
|
|
100
|
+
}
|
|
101
|
+
contextLines.push('</context>');
|
|
102
|
+
instructions = `${contextLines.join('\n')}\n\n${instructions ?? ''}`;
|
|
103
|
+
}
|
|
104
|
+
// Build stop conditions
|
|
105
|
+
const stopConditions = [
|
|
106
|
+
stepCountIs(agent.properties.maxSteps ?? 10)
|
|
107
|
+
];
|
|
108
|
+
const stopOnToolCall = agent.properties.stopOnToolCall;
|
|
109
|
+
if (stopOnToolCall) {
|
|
110
|
+
const toolNames = Array.isArray(stopOnToolCall) ? stopOnToolCall : [
|
|
111
|
+
stopOnToolCall
|
|
112
|
+
];
|
|
113
|
+
for (const name of toolNames){
|
|
114
|
+
stopConditions.push(hasToolCall(name));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const agentInstance = new ToolLoopAgent({
|
|
118
|
+
model,
|
|
119
|
+
instructions,
|
|
120
|
+
tools,
|
|
121
|
+
stopWhen: stopConditions.length === 1 ? stopConditions[0] : stopConditions,
|
|
122
|
+
maxOutputTokens: agent.properties.maxOutputTokens,
|
|
123
|
+
temperature: agent.properties.temperature,
|
|
124
|
+
toolChoice: agent.properties.toolChoice ?? 'auto',
|
|
125
|
+
providerOptions: agent.properties.providerOptions,
|
|
126
|
+
activeTools: agent.properties.activeTools,
|
|
127
|
+
topP: agent.properties.topP,
|
|
128
|
+
topK: agent.properties.topK,
|
|
129
|
+
frequencyPenalty: agent.properties.frequencyPenalty,
|
|
130
|
+
presencePenalty: agent.properties.presencePenalty,
|
|
131
|
+
seed: agent.properties.seed,
|
|
132
|
+
stopSequences: agent.properties.stopSequences,
|
|
133
|
+
maxRetries: agent.properties.maxRetries,
|
|
134
|
+
...agent.properties.prepareStep ? {
|
|
135
|
+
prepareStep: buildPrepareStep(agent.properties.prepareStep)
|
|
136
|
+
} : {},
|
|
137
|
+
...hookCallbacks,
|
|
138
|
+
...agent.properties.repairToolCall ? {
|
|
139
|
+
experimental_repairToolCall: async ({ toolCall })=>{
|
|
140
|
+
return {
|
|
141
|
+
...toolCall
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
} : {}
|
|
145
|
+
});
|
|
146
|
+
const timeoutConfig = agent.properties.timeout != null ? {
|
|
147
|
+
timeout: agent.properties.timeout
|
|
148
|
+
} : {};
|
|
149
|
+
return {
|
|
150
|
+
agentInstance,
|
|
151
|
+
mcpClients,
|
|
152
|
+
model,
|
|
153
|
+
timeoutConfig,
|
|
154
|
+
locale
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export default createToolLoopAgent;
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
*/ function createUsageAccumulator() {
|
|
16
|
+
const usage = {
|
|
17
|
+
inputTokens: 0,
|
|
18
|
+
outputTokens: 0,
|
|
19
|
+
totalTokens: 0,
|
|
20
|
+
reasoningTokens: 0,
|
|
21
|
+
cacheReadTokens: 0,
|
|
22
|
+
cacheWriteTokens: 0
|
|
23
|
+
};
|
|
24
|
+
let finishReason = 'stop';
|
|
25
|
+
function add(stepResult) {
|
|
26
|
+
const stepUsage = stepResult?.usage ?? stepResult;
|
|
27
|
+
if (stepResult?.finishReason) {
|
|
28
|
+
finishReason = stepResult.finishReason;
|
|
29
|
+
}
|
|
30
|
+
if (!stepUsage) return;
|
|
31
|
+
usage.inputTokens += stepUsage.inputTokens ?? 0;
|
|
32
|
+
usage.outputTokens += stepUsage.outputTokens ?? 0;
|
|
33
|
+
usage.totalTokens += stepUsage.totalTokens ?? 0;
|
|
34
|
+
usage.reasoningTokens += stepUsage.outputTokenDetails?.reasoningTokens ?? 0;
|
|
35
|
+
usage.cacheReadTokens += stepUsage.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
36
|
+
usage.cacheWriteTokens += stepUsage.inputTokenDetails?.cacheWriteTokens ?? 0;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
usage,
|
|
40
|
+
add,
|
|
41
|
+
getFinishReason: ()=>finishReason
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export default createUsageAccumulator;
|
package/dist/handleAgentChat.js
CHANGED
|
@@ -12,83 +12,11 @@
|
|
|
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 {
|
|
15
|
+
*/ import { convertToModelMessages, createAgentUIStream, createUIMessageStream, createUIMessageStreamResponse, generateId, generateText, pruneMessages, TypeValidationError, validateUIMessages } from 'ai';
|
|
16
16
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
function createUsageAccumulator() {
|
|
21
|
-
const usage = {
|
|
22
|
-
inputTokens: 0,
|
|
23
|
-
outputTokens: 0,
|
|
24
|
-
totalTokens: 0,
|
|
25
|
-
reasoningTokens: 0,
|
|
26
|
-
cacheReadTokens: 0,
|
|
27
|
-
cacheWriteTokens: 0
|
|
28
|
-
};
|
|
29
|
-
let finishReason = 'stop';
|
|
30
|
-
function add(stepResult) {
|
|
31
|
-
const stepUsage = stepResult?.usage ?? stepResult;
|
|
32
|
-
if (stepResult?.finishReason) {
|
|
33
|
-
finishReason = stepResult.finishReason;
|
|
34
|
-
}
|
|
35
|
-
if (!stepUsage) return;
|
|
36
|
-
usage.inputTokens += stepUsage.inputTokens ?? 0;
|
|
37
|
-
usage.outputTokens += stepUsage.outputTokens ?? 0;
|
|
38
|
-
usage.totalTokens += stepUsage.totalTokens ?? 0;
|
|
39
|
-
usage.reasoningTokens += stepUsage.outputTokenDetails?.reasoningTokens ?? 0;
|
|
40
|
-
usage.cacheReadTokens += stepUsage.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
41
|
-
usage.cacheWriteTokens += stepUsage.inputTokenDetails?.cacheWriteTokens ?? 0;
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
usage,
|
|
45
|
-
add,
|
|
46
|
-
getFinishReason: ()=>finishReason
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
// Strip non-serializable fields from agent-level hook events before sending as payload.
|
|
50
|
-
// messages excluded here — the stream-level onFinish sends UIMessage[] directly.
|
|
51
|
-
function cleanHookEvent(event) {
|
|
52
|
-
const clean = {};
|
|
53
|
-
for (const [key, value] of Object.entries(event)){
|
|
54
|
-
if (key === 'abortSignal') continue;
|
|
55
|
-
if (key === 'messages') continue;
|
|
56
|
-
if (typeof value === 'function') continue;
|
|
57
|
-
clean[key] = value;
|
|
58
|
-
}
|
|
59
|
-
return clean;
|
|
60
|
-
}
|
|
61
|
-
// Maps YAML hook names to AI SDK callback names and creates fire-and-forget callbacks.
|
|
62
|
-
// onFinish is intentionally excluded — it is handled at the stream level inside the
|
|
63
|
-
// createUIMessageStream execute function so that hooks are awaited and can return
|
|
64
|
-
// dataParts to be written to the response stream.
|
|
65
|
-
const hookMapping = {
|
|
66
|
-
onStart: 'experimental_onStart',
|
|
67
|
-
onStepStart: 'experimental_onStepStart',
|
|
68
|
-
onToolCallStart: 'experimental_onToolCallStart',
|
|
69
|
-
onToolCallFinish: 'experimental_onToolCallFinish',
|
|
70
|
-
onStepFinish: 'onStepFinish'
|
|
71
|
-
};
|
|
72
|
-
function createHookCallbacks({ hooks, callEndpoint, locale }) {
|
|
73
|
-
if (!hooks) return {};
|
|
74
|
-
const callbacks = {};
|
|
75
|
-
for (const [yamlKey, sdkKey] of Object.entries(hookMapping)){
|
|
76
|
-
const endpointIds = hooks[yamlKey];
|
|
77
|
-
if (!endpointIds || endpointIds.length === 0) continue;
|
|
78
|
-
callbacks[sdkKey] = (event)=>{
|
|
79
|
-
const payload = {
|
|
80
|
-
...cleanHookEvent(event),
|
|
81
|
-
locale
|
|
82
|
-
};
|
|
83
|
-
for (const endpointId of endpointIds){
|
|
84
|
-
callEndpoint(endpointId, {
|
|
85
|
-
payload
|
|
86
|
-
}).catch(()=>{});
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
return callbacks;
|
|
91
|
-
}
|
|
17
|
+
import createToolLoopAgent from './createToolLoopAgent.js';
|
|
18
|
+
import createUsageAccumulator from './createUsageAccumulator.js';
|
|
19
|
+
import handleAgentGenerate from './handleAgentGenerate.js';
|
|
92
20
|
// Convert data: URLs in file parts to raw base64 so the AI SDK does not attempt
|
|
93
21
|
// to download them (it only supports http/https). The mediaType field already
|
|
94
22
|
// carries the MIME type, so nothing is lost.
|
|
@@ -112,6 +40,27 @@ function convertDataUrlsToBase64(messages) {
|
|
|
112
40
|
};
|
|
113
41
|
});
|
|
114
42
|
}
|
|
43
|
+
// Drop messages that carry no parts. The AI SDK client pushes the assistant
|
|
44
|
+
// message on the stream's `start` chunk, before any content arrives, so a
|
|
45
|
+
// request that fails after that point leaves an assistant shell with
|
|
46
|
+
// `parts: []` in the client's history. Sent back, that shell fails UIMessage
|
|
47
|
+
// validation ("Message must contain at least one part") — and keeps failing on
|
|
48
|
+
// every later turn, so one transient error would otherwise kill the
|
|
49
|
+
// conversation until a reload. There is nothing in an empty message for the
|
|
50
|
+
// model anyway.
|
|
51
|
+
function dropEmptyMessages(messages) {
|
|
52
|
+
return messages.filter((msg)=>Array.isArray(msg.parts) && msg.parts.length > 0);
|
|
53
|
+
}
|
|
54
|
+
// What the client is told when a turn fails. TypeValidationError.message
|
|
55
|
+
// embeds JSON.stringify(value) — for a UIMessage validation failure that is
|
|
56
|
+
// the entire conversation, which the chat block then shows in a toast. The
|
|
57
|
+
// full error is logged server-side before this is written to the stream.
|
|
58
|
+
function clientErrorText(error) {
|
|
59
|
+
if (TypeValidationError.isInstance(error)) {
|
|
60
|
+
return 'The conversation could not be sent: a message failed validation.';
|
|
61
|
+
}
|
|
62
|
+
return error?.message ?? String(error);
|
|
63
|
+
}
|
|
115
64
|
// Concatenate the text parts of the first user message — used as the source
|
|
116
65
|
// for generateTitle.
|
|
117
66
|
function getFirstUserText(messages) {
|
|
@@ -120,96 +69,29 @@ function getFirstUserText(messages) {
|
|
|
120
69
|
return (firstUser.parts ?? []).filter((part)=>part.type === 'text').map((part)=>part.text).join('');
|
|
121
70
|
}
|
|
122
71
|
async function handleAgentChat({ connection, properties, context }) {
|
|
72
|
+
// Headless run-to-completion mode (CallAgent routine step) — dispatched here
|
|
73
|
+
// so provider agent plugins delegate unchanged in both modes.
|
|
74
|
+
if (context.mode === 'generate') {
|
|
75
|
+
return handleAgentGenerate({
|
|
76
|
+
connection,
|
|
77
|
+
properties,
|
|
78
|
+
context
|
|
79
|
+
});
|
|
80
|
+
}
|
|
123
81
|
const { agent, messages: rawMessages } = properties;
|
|
124
|
-
const messages = convertDataUrlsToBase64(rawMessages);
|
|
125
|
-
const {
|
|
82
|
+
const messages = dropEmptyMessages(convertDataUrlsToBase64(rawMessages));
|
|
83
|
+
const { agentInstance, mcpClients, model, timeoutConfig, locale } = await createToolLoopAgent({
|
|
84
|
+
connection,
|
|
126
85
|
agent,
|
|
127
86
|
context
|
|
128
87
|
});
|
|
129
|
-
const sharedState = context.agentContext?.sharedState;
|
|
130
|
-
const updatePageStateTool = buildUpdatePageStateTool({
|
|
131
|
-
sharedState
|
|
132
|
-
});
|
|
133
|
-
if (updatePageStateTool) {
|
|
134
|
-
tools['update-page-state'] = updatePageStateTool;
|
|
135
|
-
}
|
|
136
|
-
const model = connection.provider(agent.properties.model);
|
|
137
|
-
const locale = context.i18n?.active;
|
|
138
|
-
const hookCallbacks = createHookCallbacks({
|
|
139
|
-
hooks: agent.hooks,
|
|
140
|
-
callEndpoint: context.callEndpoint,
|
|
141
|
-
locale
|
|
142
|
-
});
|
|
143
|
-
// Prepend page context to instructions when pageContext is enabled
|
|
144
|
-
let instructions = agent.properties.instructions;
|
|
145
|
-
if (agent.properties.pageContext && context.agentContext) {
|
|
146
|
-
const ctx = context.agentContext;
|
|
147
|
-
const contextLines = [
|
|
148
|
-
'<context>'
|
|
149
|
-
];
|
|
150
|
-
if (ctx.pageId) contextLines.push(` pageId: ${ctx.pageId}`);
|
|
151
|
-
if (ctx.userId) contextLines.push(` userId: ${ctx.userId}`);
|
|
152
|
-
if (ctx.conversationId) contextLines.push(` conversationId: ${ctx.conversationId}`);
|
|
153
|
-
if (ctx.urlQuery && Object.keys(ctx.urlQuery).length > 0) {
|
|
154
|
-
contextLines.push(` urlQuery: ${JSON.stringify(ctx.urlQuery)}`);
|
|
155
|
-
}
|
|
156
|
-
if (ctx.sharedState && Object.keys(ctx.sharedState).length > 0) {
|
|
157
|
-
contextLines.push(` sharedState: ${JSON.stringify(ctx.sharedState)}`);
|
|
158
|
-
}
|
|
159
|
-
contextLines.push('</context>');
|
|
160
|
-
instructions = `${contextLines.join('\n')}\n\n${instructions ?? ''}`;
|
|
161
|
-
}
|
|
162
|
-
// Build stop conditions
|
|
163
|
-
const stopConditions = [
|
|
164
|
-
stepCountIs(agent.properties.maxSteps ?? 10)
|
|
165
|
-
];
|
|
166
|
-
const stopOnToolCall = agent.properties.stopOnToolCall;
|
|
167
|
-
if (stopOnToolCall) {
|
|
168
|
-
const toolNames = Array.isArray(stopOnToolCall) ? stopOnToolCall : [
|
|
169
|
-
stopOnToolCall
|
|
170
|
-
];
|
|
171
|
-
for (const name of toolNames){
|
|
172
|
-
stopConditions.push(hasToolCall(name));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const agentInstance = new ToolLoopAgent({
|
|
176
|
-
model,
|
|
177
|
-
instructions,
|
|
178
|
-
tools,
|
|
179
|
-
stopWhen: stopConditions.length === 1 ? stopConditions[0] : stopConditions,
|
|
180
|
-
maxOutputTokens: agent.properties.maxOutputTokens,
|
|
181
|
-
temperature: agent.properties.temperature,
|
|
182
|
-
toolChoice: agent.properties.toolChoice ?? 'auto',
|
|
183
|
-
providerOptions: agent.properties.providerOptions,
|
|
184
|
-
activeTools: agent.properties.activeTools,
|
|
185
|
-
topP: agent.properties.topP,
|
|
186
|
-
topK: agent.properties.topK,
|
|
187
|
-
frequencyPenalty: agent.properties.frequencyPenalty,
|
|
188
|
-
presencePenalty: agent.properties.presencePenalty,
|
|
189
|
-
seed: agent.properties.seed,
|
|
190
|
-
stopSequences: agent.properties.stopSequences,
|
|
191
|
-
maxRetries: agent.properties.maxRetries,
|
|
192
|
-
...agent.properties.prepareStep ? {
|
|
193
|
-
prepareStep: buildPrepareStep(agent.properties.prepareStep)
|
|
194
|
-
} : {},
|
|
195
|
-
...hookCallbacks,
|
|
196
|
-
...agent.properties.repairToolCall ? {
|
|
197
|
-
experimental_repairToolCall: async ({ toolCall })=>{
|
|
198
|
-
return {
|
|
199
|
-
...toolCall
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
} : {}
|
|
203
|
-
});
|
|
204
88
|
const onFinishEndpointIds = agent.hooks?.onFinish;
|
|
205
89
|
const hasOnFinishHooks = onFinishEndpointIds && onFinishEndpointIds.length > 0;
|
|
206
90
|
const hasMcpClients = mcpClients.length > 0;
|
|
207
91
|
const pruneConfig = agent.properties.prune;
|
|
208
|
-
const timeoutConfig = agent.properties.timeout != null ? {
|
|
209
|
-
timeout: agent.properties.timeout
|
|
210
|
-
} : {};
|
|
211
92
|
const titleEnabled = agent.properties.generateTitle === true;
|
|
212
93
|
const stream = createUIMessageStream({
|
|
94
|
+
onError: clientErrorText,
|
|
213
95
|
execute: async ({ writer })=>{
|
|
214
96
|
const usageAccumulator = createUsageAccumulator();
|
|
215
97
|
const steps = [];
|
|
@@ -259,52 +141,74 @@ async function handleAgentChat({ connection, properties, context }) {
|
|
|
259
141
|
});
|
|
260
142
|
}
|
|
261
143
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
messages,
|
|
267
|
-
tools: agentInstance.tools
|
|
268
|
-
});
|
|
269
|
-
const modelMessages = await convertToModelMessages(validatedMessages, {
|
|
270
|
-
tools: agentInstance.tools
|
|
271
|
-
});
|
|
272
|
-
const prunedMessages = pruneMessages({
|
|
273
|
-
messages: modelMessages,
|
|
274
|
-
...pruneConfig
|
|
275
|
-
});
|
|
276
|
-
const result = await agentInstance.stream({
|
|
277
|
-
prompt: prunedMessages,
|
|
278
|
-
...timeoutConfig,
|
|
279
|
-
onStepFinish: collectStep
|
|
280
|
-
});
|
|
281
|
-
agentStream = result.toUIMessageStream({
|
|
282
|
-
originalMessages: validatedMessages,
|
|
283
|
-
onFinish: captureMessages
|
|
284
|
-
});
|
|
285
|
-
} else {
|
|
286
|
-
// createAgentUIStream validates UIMessages, converts to ModelMessages,
|
|
287
|
-
// runs the agent, and returns a UIMessageStream — handling the full
|
|
288
|
-
// UI→model→UI conversion that ToolLoopAgent.stream() does not.
|
|
289
|
-
agentStream = await createAgentUIStream({
|
|
290
|
-
agent: agentInstance,
|
|
291
|
-
uiMessages: messages,
|
|
292
|
-
...timeoutConfig,
|
|
293
|
-
onStepFinish: collectStep,
|
|
294
|
-
onFinish: captureMessages
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
// Read the agent stream to completion before running onFinish hooks.
|
|
298
|
-
// writer.merge() is fire-and-forget (returns void in the AI SDK), so we
|
|
299
|
-
// manually read the stream to ensure hooks fire after the agent finishes.
|
|
300
|
-
const reader = agentStream.getReader();
|
|
144
|
+
// One try around validation, the agent run and the read: a UIMessage that
|
|
145
|
+
// fails validation throws from createAgentUIStream / validateUIMessages
|
|
146
|
+
// before any stream exists, and that fault has to be logged and redacted
|
|
147
|
+
// the same way as a fault mid-stream.
|
|
301
148
|
try {
|
|
149
|
+
if (pruneConfig) {
|
|
150
|
+
// Decompose createAgentUIStream so we can insert pruneMessages
|
|
151
|
+
// between the UIMessage→ModelMessage conversion and agent execution.
|
|
152
|
+
const validatedMessages = await validateUIMessages({
|
|
153
|
+
messages,
|
|
154
|
+
tools: agentInstance.tools
|
|
155
|
+
});
|
|
156
|
+
const modelMessages = await convertToModelMessages(validatedMessages, {
|
|
157
|
+
tools: agentInstance.tools
|
|
158
|
+
});
|
|
159
|
+
const prunedMessages = pruneMessages({
|
|
160
|
+
messages: modelMessages,
|
|
161
|
+
...pruneConfig
|
|
162
|
+
});
|
|
163
|
+
const result = await agentInstance.stream({
|
|
164
|
+
prompt: prunedMessages,
|
|
165
|
+
...timeoutConfig,
|
|
166
|
+
onStepFinish: collectStep
|
|
167
|
+
});
|
|
168
|
+
agentStream = result.toUIMessageStream({
|
|
169
|
+
originalMessages: validatedMessages,
|
|
170
|
+
// Without a generator the assistant message reaches onFinish with
|
|
171
|
+
// id: '' — see the createAgentUIStream call below.
|
|
172
|
+
generateMessageId: generateId,
|
|
173
|
+
onFinish: captureMessages
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
// createAgentUIStream validates UIMessages, converts to ModelMessages,
|
|
177
|
+
// runs the agent, and returns a UIMessageStream — handling the full
|
|
178
|
+
// UI→model→UI conversion that ToolLoopAgent.stream() does not.
|
|
179
|
+
agentStream = await createAgentUIStream({
|
|
180
|
+
agent: agentInstance,
|
|
181
|
+
uiMessages: messages,
|
|
182
|
+
...timeoutConfig,
|
|
183
|
+
// The AI SDK only ids the assistant message it builds when the caller
|
|
184
|
+
// supplies a generator: toUIMessageStream derives responseMessageId
|
|
185
|
+
// only if generateMessageId != null, and handleUIMessageStreamFinish
|
|
186
|
+
// then seeds the streaming state with `messageId ?? ''`, with no
|
|
187
|
+
// messageId on the start chunk to override it. Without this every
|
|
188
|
+
// assistant message an onFinish hook persists carries id: '', so a
|
|
189
|
+
// saved transcript has them all sharing one id — and a UI that keys
|
|
190
|
+
// its bubbles by message id (AgentChat does) renders the last reply in
|
|
191
|
+
// every assistant bubble when the conversation is reloaded. The client
|
|
192
|
+
// generates its own id while streaming, so the damage only shows up
|
|
193
|
+
// after a reload.
|
|
194
|
+
generateMessageId: generateId,
|
|
195
|
+
onStepFinish: collectStep,
|
|
196
|
+
onFinish: captureMessages
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Read the agent stream to completion before running onFinish hooks.
|
|
200
|
+
// writer.merge() is fire-and-forget (returns void in the AI SDK), so we
|
|
201
|
+
// manually read the stream to ensure hooks fire after the agent finishes.
|
|
202
|
+
const reader = agentStream.getReader();
|
|
302
203
|
while(true){
|
|
303
204
|
const { done, value } = await reader.read();
|
|
304
205
|
if (done) break;
|
|
305
206
|
writer.write(value);
|
|
306
207
|
}
|
|
307
208
|
} catch (error) {
|
|
209
|
+
// The client only sees the (possibly redacted) error text, so the fault
|
|
210
|
+
// is logged server-side before it is written to the stream.
|
|
211
|
+
console.error(`Agent stream failed: ${error.message}`);
|
|
308
212
|
writer.write({
|
|
309
213
|
type: 'error',
|
|
310
214
|
errorText: writer.onError(error)
|
|
@@ -0,0 +1,93 @@
|
|
|
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 createToolLoopAgent from './createToolLoopAgent.js';
|
|
16
|
+
import createUsageAccumulator from './createUsageAccumulator.js';
|
|
17
|
+
// Runs an agent headlessly to completion — no chat UI, no streaming. Used by
|
|
18
|
+
// the CallAgent routine step. Confirm tools auto-execute (no client exists to
|
|
19
|
+
// resolve an approval), update-page-state is excluded (callers pass no
|
|
20
|
+
// sharedState), and no conversation title is generated.
|
|
21
|
+
async function handleAgentGenerate({ connection, properties, context }) {
|
|
22
|
+
const { agent, prompt } = properties;
|
|
23
|
+
const { agentInstance, mcpClients, timeoutConfig, locale } = await createToolLoopAgent({
|
|
24
|
+
connection,
|
|
25
|
+
agent,
|
|
26
|
+
context,
|
|
27
|
+
autoApprove: true
|
|
28
|
+
});
|
|
29
|
+
const usageAccumulator = createUsageAccumulator();
|
|
30
|
+
let result;
|
|
31
|
+
try {
|
|
32
|
+
result = await agentInstance.generate({
|
|
33
|
+
prompt,
|
|
34
|
+
...timeoutConfig,
|
|
35
|
+
onStepFinish: (stepResult)=>usageAccumulator.add(stepResult)
|
|
36
|
+
});
|
|
37
|
+
} finally{
|
|
38
|
+
await Promise.all(mcpClients.map(({ client })=>client.close().catch(()=>{})));
|
|
39
|
+
}
|
|
40
|
+
const toolCalls = result.steps.flatMap((step)=>step.toolCalls ?? []).map(({ toolCallId, toolName, input })=>({
|
|
41
|
+
toolCallId,
|
|
42
|
+
toolName,
|
|
43
|
+
input
|
|
44
|
+
}));
|
|
45
|
+
const toolResults = result.steps.flatMap((step)=>step.toolResults ?? []).map(({ toolCallId, toolName, output })=>({
|
|
46
|
+
toolCallId,
|
|
47
|
+
toolName,
|
|
48
|
+
output
|
|
49
|
+
}));
|
|
50
|
+
// JSON round-trip guards serializability — tool outputs can carry
|
|
51
|
+
// non-serializable provider values, and the summary lands in _step.
|
|
52
|
+
const summary = JSON.parse(JSON.stringify({
|
|
53
|
+
text: result.text,
|
|
54
|
+
finishReason: result.finishReason,
|
|
55
|
+
usage: usageAccumulator.usage,
|
|
56
|
+
toolCalls,
|
|
57
|
+
toolResults
|
|
58
|
+
}));
|
|
59
|
+
// Call onFinish hooks — awaited, mirroring the stream path, but any
|
|
60
|
+
// dataParts returned by hook endpoints are ignored (there is no stream).
|
|
61
|
+
const onFinishEndpointIds = agent.hooks?.onFinish;
|
|
62
|
+
if (onFinishEndpointIds && onFinishEndpointIds.length > 0) {
|
|
63
|
+
const finishPayload = {
|
|
64
|
+
messages: result.response.messages,
|
|
65
|
+
steps: result.steps.map(({ stepNumber, text, toolCalls: calls, toolResults: results, finishReason })=>({
|
|
66
|
+
stepNumber,
|
|
67
|
+
text,
|
|
68
|
+
toolCalls: calls,
|
|
69
|
+
toolResults: results,
|
|
70
|
+
finishReason
|
|
71
|
+
})),
|
|
72
|
+
toolResults,
|
|
73
|
+
finishReason: result.finishReason,
|
|
74
|
+
isAborted: false,
|
|
75
|
+
...context.agentContext ?? {},
|
|
76
|
+
usage: usageAccumulator.usage,
|
|
77
|
+
locale
|
|
78
|
+
};
|
|
79
|
+
for (const endpointId of onFinishEndpointIds){
|
|
80
|
+
try {
|
|
81
|
+
await context.callEndpoint(endpointId, {
|
|
82
|
+
payload: finishPayload
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn(`onFinish hook "${endpointId}" failed: ${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
result: summary
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export default handleAgentGenerate;
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { generateText, jsonSchema, Output } from 'ai';
|
|
16
|
+
import buildGenerateCallOptions from './buildGenerateCallOptions.js';
|
|
17
|
+
// generateObject is deprecated in ai v6 — structured output is generateText
|
|
18
|
+
// with an Output.object spec, which also keeps both handlers on one code path.
|
|
19
|
+
async function handleGenerateObject({ model, request }) {
|
|
20
|
+
const result = await generateText({
|
|
21
|
+
model,
|
|
22
|
+
output: Output.object({
|
|
23
|
+
schema: jsonSchema(request.schema),
|
|
24
|
+
name: request.schemaName,
|
|
25
|
+
description: request.schemaDescription
|
|
26
|
+
}),
|
|
27
|
+
...buildGenerateCallOptions({
|
|
28
|
+
request
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
object: result.output,
|
|
33
|
+
reasoningText: result.reasoningText,
|
|
34
|
+
finishReason: result.finishReason,
|
|
35
|
+
usage: result.usage,
|
|
36
|
+
providerMetadata: result.providerMetadata,
|
|
37
|
+
warnings: result.warnings
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export default handleGenerateObject;
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { generateText } from 'ai';
|
|
16
|
+
import buildGenerateCallOptions from './buildGenerateCallOptions.js';
|
|
17
|
+
async function handleGenerateText({ model, request }) {
|
|
18
|
+
const result = await generateText({
|
|
19
|
+
model,
|
|
20
|
+
...buildGenerateCallOptions({
|
|
21
|
+
request
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
text: result.text,
|
|
26
|
+
reasoningText: result.reasoningText,
|
|
27
|
+
finishReason: result.finishReason,
|
|
28
|
+
usage: result.usage,
|
|
29
|
+
providerMetadata: result.providerMetadata,
|
|
30
|
+
warnings: result.warnings
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export default handleGenerateText;
|
package/dist/index.js
CHANGED
|
@@ -12,5 +12,8 @@
|
|
|
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
|
-
*/ export { default as
|
|
15
|
+
*/ export { default as createGenerateObject } from './createGenerateObject.js';
|
|
16
|
+
export { default as createGenerateText } from './createGenerateText.js';
|
|
17
|
+
export { default as handleAgentChat } from './handleAgentChat.js';
|
|
18
|
+
export { default as handleAgentGenerate } from './handleAgentGenerate.js';
|
|
16
19
|
export { default as RESERVED_PLATFORM_TOOL_NAMES } from './reservedToolNames.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/ai-utils",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,15 +17,16 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@ai-sdk/mcp": "1.0.41",
|
|
20
|
-
"@lowdefy/errors": "
|
|
21
|
-
"@lowdefy/helpers": "
|
|
20
|
+
"@lowdefy/errors": "6.0.0",
|
|
21
|
+
"@lowdefy/helpers": "6.0.0",
|
|
22
22
|
"ai": "6.0.176",
|
|
23
23
|
"glob": "13.0.6"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@jest/globals": "28.1.3",
|
|
27
|
-
"@
|
|
28
|
-
"@swc/
|
|
27
|
+
"@lowdefy/ajv": "6.0.0",
|
|
28
|
+
"@swc/cli": "0.8.1",
|
|
29
|
+
"@swc/core": "1.15.32",
|
|
29
30
|
"@swc/jest": "0.2.39",
|
|
30
31
|
"jest": "28.1.3"
|
|
31
32
|
},
|