@lowdefy/ai-utils 5.6.0 → 6.1.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.
@@ -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
+ };
@@ -43,7 +43,9 @@ function assertNotPlatformToolName(name, kind, i18n) {
43
43
  }));
44
44
  }
45
45
  }
46
- async function buildAgentTools({ agent, context, depth = 0 }) {
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
- assertNotPlatformToolName(endpointId, 'Endpoint tool', context.i18n);
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(endpointId)) {
69
- console.warn(`Endpoint tool "${endpointId}" uses a reserved key name — skipped.`);
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, endpointId, tool({
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
- setKey(tools, name, source.confirm ? {
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
- assertNotPlatformToolName(subAgentRef.agentId, 'Sub-agent', context.i18n);
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(subAgentRef.agentId)) {
170
- console.warn(`Sub-agent tool "${subAgentRef.agentId}" uses a reserved key name — skipped.`);
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, subAgentRef.agentId, tool({
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;
@@ -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 { ToolLoopAgent, convertToModelMessages, createAgentUIStream, createUIMessageStream, createUIMessageStreamResponse, generateText, pruneMessages, stepCountIs, hasToolCall, validateUIMessages } from 'ai';
15
+ */ import { convertToModelMessages, createAgentUIStream, createUIMessageStream, createUIMessageStreamResponse, generateId, generateText, pruneMessages, validateUIMessages } from 'ai';
16
16
  import { serializer } from '@lowdefy/helpers';
17
- import buildAgentTools from './buildAgentTools.js';
18
- import buildPrepareStep from './buildPrepareStep.js';
19
- import buildUpdatePageStateTool from './buildUpdatePageStateTool.js';
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 or a tool throws. The text reaches the end
55
+ // user and AgentChat config (`_event: message`), so it takes the wire policy: the
56
+ // author's message for a UserError or an auth refusal, the generic message otherwise. A
57
+ // library's message can embed anything it saw - a TypeValidationError's embeds the whole
58
+ // conversation. The full error is logged server-side.
59
+ function createClientErrorText(context) {
60
+ return function clientErrorText(error) {
61
+ return context.wireErrorMessage(error);
62
+ };
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,32 @@ 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 { tools, mcpClients } = await buildAgentTools({
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;
93
+ const clientErrorText = createClientErrorText(context);
94
+ // Every stream gets clientErrorText: the AI SDK's default onError writes a tool error or
95
+ // an in-stream error to the client raw.
212
96
  const stream = createUIMessageStream({
97
+ onError: clientErrorText,
213
98
  execute: async ({ writer })=>{
214
99
  const usageAccumulator = createUsageAccumulator();
215
100
  const steps = [];
@@ -259,52 +144,76 @@ async function handleAgentChat({ connection, properties, context }) {
259
144
  });
260
145
  }
261
146
  }
262
- if (pruneConfig) {
263
- // Decompose createAgentUIStream so we can insert pruneMessages
264
- // between the UIMessage→ModelMessage conversion and agent execution.
265
- const validatedMessages = await validateUIMessages({
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();
147
+ // One try around validation, the agent run and the read: a UIMessage that
148
+ // fails validation throws from createAgentUIStream / validateUIMessages
149
+ // before any stream exists, and that fault has to be logged and redacted
150
+ // the same way as a fault mid-stream.
301
151
  try {
152
+ if (pruneConfig) {
153
+ // Decompose createAgentUIStream so we can insert pruneMessages
154
+ // between the UIMessage→ModelMessage conversion and agent execution.
155
+ const validatedMessages = await validateUIMessages({
156
+ messages,
157
+ tools: agentInstance.tools
158
+ });
159
+ const modelMessages = await convertToModelMessages(validatedMessages, {
160
+ tools: agentInstance.tools
161
+ });
162
+ const prunedMessages = pruneMessages({
163
+ messages: modelMessages,
164
+ ...pruneConfig
165
+ });
166
+ const result = await agentInstance.stream({
167
+ prompt: prunedMessages,
168
+ ...timeoutConfig,
169
+ onStepFinish: collectStep
170
+ });
171
+ agentStream = result.toUIMessageStream({
172
+ originalMessages: validatedMessages,
173
+ // Without a generator the assistant message reaches onFinish with
174
+ // id: '' — see the createAgentUIStream call below.
175
+ generateMessageId: generateId,
176
+ onFinish: captureMessages,
177
+ onError: clientErrorText
178
+ });
179
+ } else {
180
+ // createAgentUIStream validates UIMessages, converts to ModelMessages,
181
+ // runs the agent, and returns a UIMessageStream — handling the full
182
+ // UI→model→UI conversion that ToolLoopAgent.stream() does not.
183
+ agentStream = await createAgentUIStream({
184
+ agent: agentInstance,
185
+ uiMessages: messages,
186
+ ...timeoutConfig,
187
+ // The AI SDK only ids the assistant message it builds when the caller
188
+ // supplies a generator: toUIMessageStream derives responseMessageId
189
+ // only if generateMessageId != null, and handleUIMessageStreamFinish
190
+ // then seeds the streaming state with `messageId ?? ''`, with no
191
+ // messageId on the start chunk to override it. Without this every
192
+ // assistant message an onFinish hook persists carries id: '', so a
193
+ // saved transcript has them all sharing one id — and a UI that keys
194
+ // its bubbles by message id (AgentChat does) renders the last reply in
195
+ // every assistant bubble when the conversation is reloaded. The client
196
+ // generates its own id while streaming, so the damage only shows up
197
+ // after a reload.
198
+ generateMessageId: generateId,
199
+ onStepFinish: collectStep,
200
+ onFinish: captureMessages,
201
+ onError: clientErrorText
202
+ });
203
+ }
204
+ // Read the agent stream to completion before running onFinish hooks.
205
+ // writer.merge() is fire-and-forget (returns void in the AI SDK), so we
206
+ // manually read the stream to ensure hooks fire after the agent finishes.
207
+ const reader = agentStream.getReader();
302
208
  while(true){
303
209
  const { done, value } = await reader.read();
304
210
  if (done) break;
305
211
  writer.write(value);
306
212
  }
307
213
  } catch (error) {
214
+ // The client only sees the (possibly redacted) error text, so the fault
215
+ // is logged server-side before it is written to the stream.
216
+ console.error(`Agent stream failed: ${error.message}`);
308
217
  writer.write({
309
218
  type: 'error',
310
219
  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 handleAgentChat } from './handleAgentChat.js';
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": "5.6.0",
3
+ "version": "6.1.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": "5.6.0",
21
- "@lowdefy/helpers": "5.6.0",
20
+ "@lowdefy/errors": "6.1.0",
21
+ "@lowdefy/helpers": "6.1.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
- "@swc/cli": "0.8.0",
28
- "@swc/core": "1.15.18",
27
+ "@lowdefy/ajv": "6.1.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
  },