@lowdefy/build 5.2.0 → 5.3.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,249 @@
1
+ /* eslint-disable no-param-reassign */ /*
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 path from 'path';
16
+ import fs from 'fs';
17
+ import { type } from '@lowdefy/helpers';
18
+ import { ConfigError, ConfigWarning } from '@lowdefy/errors';
19
+ import { RESERVED_PLATFORM_TOOL_NAMES } from '@lowdefy/ai-utils';
20
+ import countOperators from '../utils/countOperators.js';
21
+ import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
22
+ function detectCycles(agents) {
23
+ const graph = {};
24
+ for (const agent of agents){
25
+ graph[agent.agentId] = (agent.agents ?? []).map((ref)=>ref.agentId);
26
+ }
27
+ const visited = new Set();
28
+ const inStack = new Set();
29
+ function dfs(id) {
30
+ if (inStack.has(id)) return id;
31
+ if (visited.has(id)) return null;
32
+ visited.add(id);
33
+ inStack.add(id);
34
+ for (const neighbor of graph[id] ?? []){
35
+ const cycleNode = dfs(neighbor);
36
+ if (cycleNode !== null) return cycleNode;
37
+ }
38
+ inStack.delete(id);
39
+ return null;
40
+ }
41
+ for (const id of Object.keys(graph)){
42
+ const cycleNode = dfs(id);
43
+ if (cycleNode !== null) {
44
+ return cycleNode;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ function buildAgents({ components, context }) {
50
+ if (!type.isArray(components.agents)) {
51
+ return components;
52
+ }
53
+ context.agentIds = new Set();
54
+ const checkDuplicateAgentId = createCheckDuplicateId({
55
+ message: 'Duplicate agentId "{{ id }}".'
56
+ });
57
+ components.agents.forEach((agent)=>{
58
+ const configKey = agent['~k'];
59
+ // Check duplicates
60
+ checkDuplicateAgentId({
61
+ id: agent.id,
62
+ configKey
63
+ });
64
+ // Track type usage for buildTypes validation
65
+ context.typeCounters.agents.increment(agent.type, configKey);
66
+ // Validate connectionId is provided
67
+ if (type.isNone(agent.connectionId)) {
68
+ throw new ConfigError(`Agent connectionId is not defined at "${agent.id}".`, {
69
+ configKey
70
+ });
71
+ }
72
+ // Validate connectionId references an existing connection
73
+ // Connections may have been renamed by buildConnections:
74
+ // connection.connectionId = original id, connection.id = 'connection:' + original id
75
+ const connectionExists = (components.connections ?? []).some((c)=>c.id === agent.connectionId || c.connectionId === agent.connectionId);
76
+ if (!connectionExists) {
77
+ throw new ConfigError(`Agent "${agent.id}" references connectionId "${agent.connectionId}" which does not exist.`, {
78
+ configKey
79
+ });
80
+ }
81
+ // Validate model is defined
82
+ if (type.isNone(agent.properties?.model)) {
83
+ throw new ConfigError(`Agent "model" is not defined at "${agent.id}".`, {
84
+ configKey
85
+ });
86
+ }
87
+ // Normalize tool strings to objects
88
+ agent.tools = (agent.tools ?? []).map((tool)=>{
89
+ if (type.isString(tool)) {
90
+ return {
91
+ endpointId: tool
92
+ };
93
+ }
94
+ return tool;
95
+ });
96
+ // Validate tools reference existing API endpoints with required tool metadata
97
+ agent.tools.forEach((toolConfig)=>{
98
+ if (RESERVED_PLATFORM_TOOL_NAMES.includes(toolConfig.endpointId)) {
99
+ throw new ConfigError(`Agent "${agent.id}" tool "${toolConfig.endpointId}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
100
+ configKey
101
+ });
102
+ }
103
+ const endpoint = (components.api ?? []).find((e)=>e.id === toolConfig.endpointId || e.endpointId === toolConfig.endpointId);
104
+ if (!endpoint) {
105
+ throw new ConfigError(`Agent "${agent.id}" references tool endpoint "${toolConfig.endpointId}" which does not exist.`, {
106
+ configKey
107
+ });
108
+ }
109
+ if (type.isNone(endpoint.description)) {
110
+ throw new ConfigError(`Endpoint "${toolConfig.endpointId}" is used as an agent tool but does not have a "description".`, {
111
+ configKey: endpoint['~k']
112
+ });
113
+ }
114
+ if (type.isNone(endpoint.payloadSchema)) {
115
+ throw new ConfigError(`Endpoint "${toolConfig.endpointId}" is used as an agent tool but does not have a "payloadSchema".`, {
116
+ configKey: endpoint['~k']
117
+ });
118
+ }
119
+ });
120
+ // Normalize MCP string shorthand to connectionId objects (same pattern as tools)
121
+ agent.mcp = (agent.mcp ?? []).map((mcp)=>{
122
+ if (type.isString(mcp)) {
123
+ return {
124
+ connectionId: mcp
125
+ };
126
+ }
127
+ return mcp;
128
+ });
129
+ // Validate MCP sources
130
+ agent.mcp.forEach((mcpSource, index)=>{
131
+ if (!type.isNone(mcpSource.connectionId)) {
132
+ // Validate connectionId references an existing connection
133
+ const mcpConnectionExists = (components.connections ?? []).some((c)=>c.id === mcpSource.connectionId || c.connectionId === mcpSource.connectionId);
134
+ if (!mcpConnectionExists) {
135
+ throw new ConfigError(`Agent "${agent.id}" "mcp" source at index ${index} references connection "${mcpSource.connectionId}" which does not exist.`, {
136
+ configKey
137
+ });
138
+ }
139
+ } else if (mcpSource.transport === 'stdio') {
140
+ if (type.isNone(mcpSource.command)) {
141
+ throw new ConfigError(`Agent "${agent.id}" "mcp" source at index ${index} uses stdio transport but is missing "command".`, {
142
+ configKey
143
+ });
144
+ }
145
+ } else {
146
+ if (type.isNone(mcpSource.url)) {
147
+ throw new ConfigError(`Agent "${agent.id}" "mcp" source at index ${index} is missing "url".`, {
148
+ configKey
149
+ });
150
+ }
151
+ }
152
+ });
153
+ // Validate hooks reference existing API endpoints
154
+ const hookNames = [
155
+ 'onStart',
156
+ 'onStepStart',
157
+ 'onToolCallStart',
158
+ 'onToolCallFinish',
159
+ 'onStepFinish',
160
+ 'onFinish'
161
+ ];
162
+ hookNames.forEach((hookName)=>{
163
+ (agent.hooks?.[hookName] ?? []).forEach((endpointId)=>{
164
+ const endpoint = (components.api ?? []).find((e)=>e.id === endpointId || e.endpointId === endpointId);
165
+ if (!endpoint) {
166
+ throw new ConfigError(`Agent "${agent.id}" hook "${hookName}" references endpoint "${endpointId}" which does not exist.`, {
167
+ configKey
168
+ });
169
+ }
170
+ });
171
+ });
172
+ // Normalize sub-agent strings to objects (same pattern as tools/mcp)
173
+ agent.agents = (agent.agents ?? []).map((ref)=>{
174
+ if (type.isString(ref)) {
175
+ return {
176
+ agentId: ref
177
+ };
178
+ }
179
+ return ref;
180
+ });
181
+ // Validate fileSystem basePath if present
182
+ if (agent.properties?.fileSystem) {
183
+ const basePath = agent.properties.fileSystem.basePath;
184
+ if (!type.isString(basePath)) {
185
+ throw new ConfigError(`Agent "${agent.id}" fileSystem.basePath is not a string.`, {
186
+ received: basePath,
187
+ configKey
188
+ });
189
+ }
190
+ const resolved = path.resolve(context.directories.config, basePath);
191
+ if (!fs.existsSync(resolved)) {
192
+ throw new ConfigError(`Agent "${agent.id}" fileSystem.basePath "${basePath}" does not exist.`, {
193
+ configKey
194
+ });
195
+ }
196
+ }
197
+ // Rename id to internal format
198
+ agent.agentId = agent.id;
199
+ context.agentIds.add(agent.agentId);
200
+ agent.id = `agent:${agent.agentId}`;
201
+ // Count server operators in properties
202
+ countOperators(agent.properties ?? {}, {
203
+ counter: context.typeCounters.operators.server
204
+ });
205
+ });
206
+ // Second pass: validate sub-agent references (needs all agentIds collected)
207
+ components.agents.forEach((agent)=>{
208
+ const configKey = agent['~k'];
209
+ agent.agents.forEach((subAgentRef)=>{
210
+ // Validate sub-agent reference exists
211
+ if (!context.agentIds.has(subAgentRef.agentId)) {
212
+ throw new ConfigError(`Agent "${agent.agentId}" references sub-agent "${subAgentRef.agentId}" which does not exist.`, {
213
+ configKey
214
+ });
215
+ }
216
+ // Reserved platform tool name guard for sub-agents
217
+ if (RESERVED_PLATFORM_TOOL_NAMES.includes(subAgentRef.agentId)) {
218
+ throw new ConfigError(`Agent "${agent.agentId}" sub-agent "${subAgentRef.agentId}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
219
+ configKey
220
+ });
221
+ }
222
+ // Check for name collision with endpoint tools
223
+ const hasToolCollision = agent.tools.some((toolConfig)=>toolConfig.endpointId === subAgentRef.agentId);
224
+ if (hasToolCollision) {
225
+ throw new ConfigError(`Agent "${agent.agentId}" sub-agent "${subAgentRef.agentId}" conflicts with an endpoint tool of the same name.`, {
226
+ configKey
227
+ });
228
+ }
229
+ // Warn if sub-agent has tools with confirm: true (unsupported in sub-agent context)
230
+ const subAgent = components.agents.find((a)=>a.agentId === subAgentRef.agentId);
231
+ const hasConfirmTools = (subAgent?.tools ?? []).some((t)=>t.confirm);
232
+ if (hasConfirmTools) {
233
+ context.handleWarning(new ConfigWarning(`Agent "${subAgentRef.agentId}" has tools with confirm: true, but tool approval is not supported in sub-agent context. Tools will auto-execute when called as a sub-agent.`, {
234
+ configKey
235
+ }));
236
+ }
237
+ });
238
+ });
239
+ // Detect circular sub-agent references
240
+ const cycleNode = detectCycles(components.agents);
241
+ if (cycleNode !== null) {
242
+ const agent = components.agents.find((a)=>a.agentId === cycleNode);
243
+ throw new ConfigError(`Circular sub-agent reference detected involving "${cycleNode}".`, {
244
+ configKey: agent?.['~k']
245
+ });
246
+ }
247
+ return components;
248
+ }
249
+ export default buildAgents;
@@ -22,6 +22,7 @@ function getPluginPackages({ components }) {
22
22
  });
23
23
  }
24
24
  getPackages(components.types.actions);
25
+ getPackages(components.types.agents);
25
26
  getPackages(components.types.auth.adapters);
26
27
  getPackages(components.types.auth.callbacks);
27
28
  getPackages(components.types.auth.events);
@@ -53,6 +54,10 @@ function buildImportsDev({ components, context }) {
53
54
  pluginPackages,
54
55
  map: context.typesMap.actions
55
56
  }),
57
+ agents: buildImportClassDev({
58
+ pluginPackages,
59
+ map: context.typesMap.agents
60
+ }),
56
61
  auth: {
57
62
  adapters: buildImportClassDev({
58
63
  pluginPackages,
@@ -25,6 +25,7 @@ function buildImportsProd({ components, context }) {
25
25
  const blocks = buildImportClassProd(components.types.blocks);
26
26
  return {
27
27
  actions: buildImportClassProd(components.types.actions),
28
+ agents: buildImportClassProd(components.types.agents),
28
29
  auth: {
29
30
  adapters: buildImportClassProd(components.types.auth.adapters),
30
31
  callbacks: buildImportClassProd(components.types.auth.callbacks),
@@ -65,6 +65,7 @@ function buildTypes({ components, context }) {
65
65
  typeCounters.actions.increment('SetDarkMode');
66
66
  components.types = {
67
67
  actions: {},
68
+ agents: {},
68
69
  auth: {
69
70
  adapters: {},
70
71
  callbacks: {},
@@ -86,6 +87,12 @@ function buildTypes({ components, context }) {
86
87
  store: components.types.actions,
87
88
  typeClass: 'Action'
88
89
  });
90
+ buildTypeClass(context, {
91
+ counter: typeCounters.agents,
92
+ definitions: context.typesMap.agents,
93
+ store: components.types.agents,
94
+ typeClass: 'Agent'
95
+ });
89
96
  buildTypeClass(context, {
90
97
  counter: typeCounters.auth.adapters,
91
98
  definitions: context.typesMap.auth.adapters,
@@ -0,0 +1,45 @@
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 path from 'path';
16
+ import fs from 'fs';
17
+ import { copyFileOrDirectory } from '@lowdefy/node-utils';
18
+ async function copyAgentFileSystems({ components, context }) {
19
+ const basePaths = [];
20
+ const seen = new Set();
21
+ for (const agent of components.agents ?? []){
22
+ const basePath = agent.properties?.fileSystem?.basePath;
23
+ if (!basePath || typeof basePath !== 'string') continue;
24
+ if (seen.has(basePath)) continue;
25
+ seen.add(basePath);
26
+ basePaths.push(basePath);
27
+ }
28
+ // Manifest is consumed by next.config.js to populate outputFileTracingIncludes,
29
+ // so the Next.js tracer bundles these directories on Vercel and standalone builds.
30
+ await context.writeBuildArtifact('agentFileSystems.json', JSON.stringify(basePaths));
31
+ if (context.directories.config === context.directories.server) return;
32
+ for (const basePath of basePaths){
33
+ const source = path.resolve(context.directories.config, basePath);
34
+ if (!fs.existsSync(source)) continue;
35
+ const dest = path.resolve(context.directories.server, basePath);
36
+ try {
37
+ await copyFileOrDirectory(source, dest);
38
+ } catch (err) {
39
+ throw new Error(`Failed to copy fileSystem basePath "${basePath}" to server directory: ${err.message}`, {
40
+ cause: err
41
+ });
42
+ }
43
+ }
44
+ }
45
+ export default copyAgentFileSystems;
@@ -25,6 +25,7 @@ async function updateServerPackageJson({ components, context }) {
25
25
  });
26
26
  }
27
27
  getPackages(components.types.actions);
28
+ getPackages(components.types.agents);
28
29
  getPackages(components.types.auth.adapters);
29
30
  getPackages(components.types.auth.callbacks);
30
31
  getPackages(components.types.auth.events);
@@ -23,6 +23,7 @@ import addKeys from '../addKeys.js';
23
23
  import buildApp from '../buildApp.js';
24
24
  import buildAuth from '../buildAuth/buildAuth.js';
25
25
  import buildConnections from '../buildConnections.js';
26
+ import buildAgents from '../buildAgents.js';
26
27
  import buildApi from '../buildApi/buildApi.js';
27
28
  import buildLogger from '../buildLogger.js';
28
29
  import buildImports from '../buildImports/buildImports.js';
@@ -32,6 +33,7 @@ import buildModules from '../buildModules.js';
32
33
  import buildRefs from '../buildRefs/buildRefs.js';
33
34
  import buildTypes from '../buildTypes.js';
34
35
  import cleanBuildDirectory from '../cleanBuildDirectory.js';
36
+ import copyAgentFileSystems from '../copyAgentFileSystems.js';
35
37
  import copyPublicFolder from '../copyPublicFolder.js';
36
38
  import testSchema from '../testSchema.js';
37
39
  import validateConfig from '../validateConfig.js';
@@ -39,6 +41,7 @@ import writeApp from '../writeApp.js';
39
41
  import writeAuth from '../writeAuth.js';
40
42
  import writeConfig from '../writeConfig.js';
41
43
  import writeConnections from '../writeConnections.js';
44
+ import writeAgents from '../writeAgents.js';
42
45
  import writeApi from '../writeApi.js';
43
46
  import writeGlobal from '../writeGlobal.js';
44
47
  import writeJs from '../buildJs/writeJs.js';
@@ -146,6 +149,10 @@ async function shallowBuild(options) {
146
149
  components,
147
150
  context
148
151
  });
152
+ tryBuildStep(buildAgents, 'buildAgents', {
153
+ components,
154
+ context
155
+ });
149
156
  const { pageRegistry, sourcelessPageArtifacts } = buildShallowPages({
150
157
  components,
151
158
  context
@@ -207,6 +214,10 @@ async function shallowBuild(options) {
207
214
  components,
208
215
  context
209
216
  });
217
+ await writeAgents({
218
+ components,
219
+ context
220
+ });
210
221
  await writeConfig({
211
222
  components,
212
223
  context
@@ -265,6 +276,10 @@ async function shallowBuild(options) {
265
276
  components,
266
277
  context
267
278
  });
279
+ await copyAgentFileSystems({
280
+ components,
281
+ context
282
+ });
268
283
  return {
269
284
  components,
270
285
  pageRegistry,
@@ -156,6 +156,11 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
156
156
  validateRequiredVars(varDefs, entry.vars ?? {}, entry.id, entry.source);
157
157
  // Validate plugin dependencies against app's declared plugins
158
158
  const requiredPlugins = manifest.plugins ?? [];
159
+ for (const plugin of requiredPlugins){
160
+ if (!type.isString(plugin.version)) {
161
+ throw new ConfigError(`Module "${entry.id}": plugin "${plugin.name}" must declare a "version" ` + `(semver range) in module.lowdefy.yaml.`);
162
+ }
163
+ }
159
164
  const appPlugins = (context.plugins ?? []).reduce((map, p)=>map.set(p.name, p.version), new Map());
160
165
  for (const plugin of requiredPlugins){
161
166
  if (context.defaultPackageNames.has(plugin.name)) {
@@ -0,0 +1,26 @@
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, serializer } from '@lowdefy/helpers';
16
+ async function writeAgents({ components, context }) {
17
+ if (type.isNone(components.agents)) return;
18
+ if (!type.isArray(components.agents)) {
19
+ throw new Error(`Agents is not an array.`);
20
+ }
21
+ const writePromises = components.agents.map(async (agent)=>{
22
+ await context.writeBuildArtifact(`agents/${agent.agentId}.json`, serializer.serializeToString(agent));
23
+ });
24
+ return Promise.all(writePromises);
25
+ }
26
+ export default writeAgents;
@@ -0,0 +1,22 @@
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 generateImportFile from './generateImportFile.js';
16
+ async function writeAgentImports({ components, context }) {
17
+ await context.writeBuildArtifact('plugins/agents.js', generateImportFile({
18
+ imports: components.imports.agents,
19
+ importPath: 'agents'
20
+ }));
21
+ }
22
+ export default writeAgentImports;
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */ import writeActionImports from './writeActionImports.js';
16
16
  import writeActionSchemaMap from './writeActionSchemaMap.js';
17
+ import writeAgentImports from './writeAgentImports.js';
17
18
  import writeAuthImports from './writeAuthImports.js';
18
19
  import writeBlockImports from './writeBlockImports.js';
19
20
  import writeBlockSchemaMap from './writeBlockSchemaMap.js';
@@ -31,6 +32,10 @@ async function writePluginImports({ components, context }) {
31
32
  components,
32
33
  context
33
34
  });
35
+ await writeAgentImports({
36
+ components,
37
+ context
38
+ });
34
39
  await writeAuthImports({
35
40
  components,
36
41
  context
@@ -23,6 +23,7 @@ import defaultTypesMap from './defaultTypesMap.js';
23
23
  function createContext({ customTypesMap, directories, logger, refResolver, stage = 'prod' }) {
24
24
  const context = {
25
25
  defaultPackageNames: new Set(defaultPackages),
26
+ agentIds: new Set(),
26
27
  connectionIds: new Set(),
27
28
  directories,
28
29
  errors: [],
@@ -42,6 +43,7 @@ function createContext({ customTypesMap, directories, logger, refResolver, stage
42
43
  stage,
43
44
  typeCounters: {
44
45
  actions: createCounter(),
46
+ agents: createCounter(),
45
47
  auth: {
46
48
  adapters: createCounter(),
47
49
  callbacks: createCounter(),
@@ -17,6 +17,7 @@
17
17
  '@lowdefy/actions-pdf-make',
18
18
  '@lowdefy/blocks-aggrid',
19
19
  '@lowdefy/blocks-antd',
20
+ '@lowdefy/blocks-antd-x',
20
21
  '@lowdefy/blocks-basic',
21
22
  '@lowdefy/blocks-diff',
22
23
  '@lowdefy/blocks-echarts',
@@ -25,7 +26,13 @@
25
26
  '@lowdefy/blocks-markdown',
26
27
  '@lowdefy/blocks-qr',
27
28
  '@lowdefy/blocks-tiptap',
29
+ '@lowdefy/ai-utils',
30
+ '@lowdefy/connection-ai-gateway',
31
+ '@lowdefy/connection-anthropic',
28
32
  '@lowdefy/connection-axios-http',
33
+ '@lowdefy/connection-google',
34
+ '@lowdefy/connection-mcp',
35
+ '@lowdefy/connection-openai',
29
36
  '@lowdefy/connection-elasticsearch',
30
37
  '@lowdefy/connection-test',
31
38
  '@lowdefy/connection-google-sheets',