@prompd/cli 0.5.0-beta.2 → 0.5.0-beta.4

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.
@@ -21,6 +21,169 @@ const path_1 = require("path");
21
21
  const vm_1 = __importDefault(require("vm"));
22
22
  const workflowParser_1 = require("./workflowParser");
23
23
  const memoryBackend_1 = require("./memoryBackend");
24
+ /**
25
+ * Node types that represent callable tools.
26
+ * Must match TOOL_NODE_TYPES in the frontend's workflowTypes.ts.
27
+ */
28
+ const TOOL_NODE_TYPES = new Set([
29
+ 'tool', 'mcp-tool', 'web-search', 'skill', 'api',
30
+ 'command', 'code', 'claude-code', 'database-query',
31
+ ]);
32
+ /** Node types allowed as children in tool containers (tool-call-router, chat-agent) */
33
+ const TOOL_CONTAINER_CHILD_TYPES = new Set([
34
+ ...TOOL_NODE_TYPES, 'tool-call-parser',
35
+ ]);
36
+ /**
37
+ * Convert any tool-like workflow node to an AgentTool definition.
38
+ * Handles all node types that extend BaseToolNodeData (tool, mcp-tool, web-search,
39
+ * skill, command, code, claude-code, database-query, api).
40
+ */
41
+ function nodeToAgentTool(toolNode) {
42
+ const data = toolNode.data;
43
+ const nodeType = toolNode.type || 'tool';
44
+ // Read BaseToolNodeData fields (all tool-like nodes have these)
45
+ const toolName = data.toolName || data.label || nodeType;
46
+ const description = data.description || `Tool: ${toolName}`;
47
+ const parameterSchema = data.parameterSchema;
48
+ // Determine AgentTool toolType based on node type
49
+ let agentToolType = 'function';
50
+ let originalToolType = nodeType;
51
+ if (nodeType === 'tool') {
52
+ // ToolNode has its own toolType field
53
+ const tt = data.toolType || 'function';
54
+ originalToolType = tt;
55
+ agentToolType = tt === 'http' ? 'http' : tt === 'mcp' ? 'mcp' : 'function';
56
+ }
57
+ else if (nodeType === 'mcp-tool') {
58
+ agentToolType = 'mcp';
59
+ }
60
+ else if (nodeType === 'api') {
61
+ agentToolType = 'http';
62
+ }
63
+ else if (nodeType === 'command' || nodeType === 'claude-code') {
64
+ agentToolType = 'command';
65
+ }
66
+ else if (nodeType === 'code') {
67
+ agentToolType = 'code';
68
+ }
69
+ else if (nodeType === 'web-search') {
70
+ agentToolType = 'web-search';
71
+ }
72
+ else if (nodeType === 'database-query') {
73
+ agentToolType = 'database-query';
74
+ }
75
+ // skill → 'function' (default)
76
+ // Auto-generate parameterSchema for node types that have known inputs
77
+ // but no explicit schema set by the user
78
+ let resolvedParameters = parameterSchema;
79
+ if (!resolvedParameters || !resolvedParameters.properties || Object.keys(resolvedParameters.properties).length === 0) {
80
+ if (nodeType === 'web-search') {
81
+ resolvedParameters = {
82
+ type: 'object',
83
+ properties: {
84
+ query: { type: 'string', description: 'The search query to look up on the web' },
85
+ },
86
+ required: ['query'],
87
+ };
88
+ }
89
+ else if (nodeType === 'database-query') {
90
+ const dbData = data;
91
+ // For MongoDB: the LLM provides a JSON query document and optionally a collection
92
+ // For SQL: the LLM provides the SQL query and optionally parameters
93
+ // The node's configured values serve as defaults
94
+ resolvedParameters = {
95
+ type: 'object',
96
+ properties: {
97
+ query: {
98
+ type: 'string',
99
+ description: dbData.collection
100
+ ? `Query to execute. For MongoDB, provide a JSON filter document (e.g. {"name": "John"}). Default collection: ${dbData.collection}`
101
+ : 'SQL query or database command to execute',
102
+ },
103
+ ...(dbData.collection ? {
104
+ collection: {
105
+ type: 'string',
106
+ description: `MongoDB collection name (default: ${dbData.collection})`,
107
+ },
108
+ } : {}),
109
+ },
110
+ required: ['query'],
111
+ };
112
+ }
113
+ else if (nodeType === 'command') {
114
+ resolvedParameters = {
115
+ type: 'object',
116
+ properties: {
117
+ input: { type: 'string', description: 'Input to pass to the command' },
118
+ },
119
+ };
120
+ }
121
+ else if (nodeType === 'api') {
122
+ resolvedParameters = {
123
+ type: 'object',
124
+ properties: {
125
+ input: { type: 'string', description: 'Input data for the API request' },
126
+ },
127
+ };
128
+ }
129
+ }
130
+ const agentTool = {
131
+ name: toolName,
132
+ description,
133
+ toolType: agentToolType,
134
+ parameters: resolvedParameters,
135
+ _toolNodeId: toolNode.id,
136
+ _originalToolType: originalToolType,
137
+ };
138
+ // Add type-specific config
139
+ if (nodeType === 'tool') {
140
+ const toolData = data;
141
+ if (toolData.toolType === 'http') {
142
+ agentTool.httpConfig = {
143
+ method: toolData.httpMethod || 'GET',
144
+ url: toolData.httpUrl || '',
145
+ headers: toolData.httpHeaders,
146
+ bodyTemplate: toolData.httpBody,
147
+ };
148
+ }
149
+ else if (toolData.toolType === 'mcp') {
150
+ agentTool.mcpConfig = {
151
+ serverUrl: toolData.mcpServerUrl,
152
+ serverName: toolData.mcpServerName,
153
+ };
154
+ }
155
+ }
156
+ else if (nodeType === 'mcp-tool') {
157
+ const mcpData = data;
158
+ agentTool.mcpConfig = {
159
+ serverUrl: mcpData.serverConfig?.serverUrl,
160
+ serverName: mcpData.serverConfig?.serverName,
161
+ };
162
+ }
163
+ else if (nodeType === 'api') {
164
+ const apiData = data;
165
+ agentTool.httpConfig = {
166
+ method: apiData.method || 'GET',
167
+ url: apiData.url || '',
168
+ headers: apiData.headers,
169
+ bodyTemplate: apiData.body,
170
+ };
171
+ // Carry connectionId so execution can resolve baseUrl from the http-api connection
172
+ if (data.connectionId) {
173
+ agentTool._connectionId = data.connectionId;
174
+ }
175
+ }
176
+ else if (nodeType === 'command') {
177
+ const cmdData = data;
178
+ agentTool.commandConfig = {
179
+ executable: cmdData.command || '',
180
+ args: cmdData.args?.join(' '),
181
+ cwd: cmdData.cwd,
182
+ requiresApproval: cmdData.requiresApproval,
183
+ };
184
+ }
185
+ return agentTool;
186
+ }
24
187
  /**
25
188
  * Send error notification to webhook endpoint
26
189
  * This is fire-and-forget - errors are logged but don't affect execution
@@ -3640,6 +3803,321 @@ async function emitAgentCheckpoint(event, options, workflowFile) {
3640
3803
  * history is captured and included in the output for downstream analysis.
3641
3804
  * Use a Checkpoint node connected to the agent output to inspect agent state.
3642
3805
  */
3806
+ /**
3807
+ * Build memory tools based on the docked memory node's mode.
3808
+ *
3809
+ * When a memory node is docked to an agent's 'memory' handle, the tools
3810
+ * adapt to the memory mode:
3811
+ * - kv: memory_get, memory_set, memory_delete, memory_list
3812
+ * - conversation: memory_get_history, memory_append, memory_clear_history
3813
+ * - cache: memory_get (with TTL info), memory_set (with TTL), memory_delete, memory_list
3814
+ *
3815
+ * When no memory node is docked, defaults to KV tools for backward compatibility.
3816
+ * Scope and namespace are pre-filled from the memory node config to simplify LLM usage.
3817
+ */
3818
+ function buildMemoryTools(agentNodeId, workflowFile) {
3819
+ // Find docked memory node
3820
+ let memoryMode = 'kv';
3821
+ let defaultScope = 'workflow';
3822
+ let defaultNamespace = '';
3823
+ let defaultConversationId = 'default';
3824
+ let maxMessages = 0;
3825
+ if (workflowFile) {
3826
+ const dockedMemoryNode = workflowFile.nodes.find(n => {
3827
+ const nodeData = n.data;
3828
+ return n.type === 'memory' &&
3829
+ nodeData.dockedTo?.nodeId === agentNodeId &&
3830
+ nodeData.dockedTo?.handleId === 'memory';
3831
+ });
3832
+ // Also check for edge-connected memory nodes (non-docked)
3833
+ const edgeConnectedMemoryNode = !dockedMemoryNode ? workflowFile.nodes.find(n => {
3834
+ if (n.type !== 'memory')
3835
+ return false;
3836
+ return workflowFile.edges.some(e => e.source === agentNodeId && e.sourceHandle === 'memory' && e.target === n.id);
3837
+ }) : undefined;
3838
+ const memoryNode = dockedMemoryNode || edgeConnectedMemoryNode;
3839
+ if (memoryNode) {
3840
+ const memData = memoryNode.data;
3841
+ memoryMode = memData.mode || 'kv';
3842
+ defaultScope = memData.scope || 'workflow';
3843
+ defaultNamespace = memData.namespace || '';
3844
+ if (memoryMode === 'conversation') {
3845
+ defaultConversationId = memData.conversationId || 'default';
3846
+ maxMessages = memData.maxMessages ?? 0;
3847
+ }
3848
+ }
3849
+ }
3850
+ const scopeDescription = defaultScope === 'execution'
3851
+ ? 'execution: data is cleared when this workflow run ends'
3852
+ : defaultScope === 'global'
3853
+ ? 'global: data shared across all workflows'
3854
+ : 'workflow: data persists across executions of this workflow';
3855
+ switch (memoryMode) {
3856
+ case 'conversation': {
3857
+ return [
3858
+ {
3859
+ name: 'memory_get_history',
3860
+ description: `Retrieve conversation history. Returns an array of messages with role and content.${maxMessages > 0 ? ` Sliding window: last ${maxMessages} messages.` : ''}`,
3861
+ toolType: 'function',
3862
+ parameters: {
3863
+ type: 'object',
3864
+ properties: {
3865
+ conversation_id: {
3866
+ type: 'string',
3867
+ description: `Conversation thread ID (default: "${defaultConversationId}")`,
3868
+ },
3869
+ scope: {
3870
+ type: 'string',
3871
+ enum: ['workflow', 'global'],
3872
+ description: scopeDescription,
3873
+ },
3874
+ namespace: {
3875
+ type: 'string',
3876
+ description: `Namespace for isolation (default: "${defaultNamespace || 'default'}")`,
3877
+ },
3878
+ },
3879
+ required: [],
3880
+ },
3881
+ },
3882
+ {
3883
+ name: 'memory_append',
3884
+ description: 'Append a message to conversation history. The message is added with a role and content.',
3885
+ toolType: 'function',
3886
+ parameters: {
3887
+ type: 'object',
3888
+ properties: {
3889
+ role: {
3890
+ type: 'string',
3891
+ enum: ['user', 'assistant', 'system'],
3892
+ description: 'The role of the message sender',
3893
+ },
3894
+ content: {
3895
+ type: 'string',
3896
+ description: 'The message content to append',
3897
+ },
3898
+ conversation_id: {
3899
+ type: 'string',
3900
+ description: `Conversation thread ID (default: "${defaultConversationId}")`,
3901
+ },
3902
+ scope: {
3903
+ type: 'string',
3904
+ enum: ['workflow', 'global'],
3905
+ description: scopeDescription,
3906
+ },
3907
+ namespace: {
3908
+ type: 'string',
3909
+ description: `Namespace for isolation (default: "${defaultNamespace || 'default'}")`,
3910
+ },
3911
+ },
3912
+ required: ['role', 'content'],
3913
+ },
3914
+ },
3915
+ {
3916
+ name: 'memory_clear_history',
3917
+ description: 'Clear all messages from a conversation history.',
3918
+ toolType: 'function',
3919
+ parameters: {
3920
+ type: 'object',
3921
+ properties: {
3922
+ conversation_id: {
3923
+ type: 'string',
3924
+ description: `Conversation thread ID (default: "${defaultConversationId}")`,
3925
+ },
3926
+ scope: {
3927
+ type: 'string',
3928
+ enum: ['workflow', 'global'],
3929
+ description: scopeDescription,
3930
+ },
3931
+ namespace: {
3932
+ type: 'string',
3933
+ description: `Namespace for isolation (default: "${defaultNamespace || 'default'}")`,
3934
+ },
3935
+ },
3936
+ required: [],
3937
+ },
3938
+ },
3939
+ ];
3940
+ }
3941
+ case 'cache': {
3942
+ return [
3943
+ {
3944
+ name: 'memory_get',
3945
+ description: 'Retrieve a cached value. Returns null if expired or not found.',
3946
+ toolType: 'function',
3947
+ parameters: {
3948
+ type: 'object',
3949
+ properties: {
3950
+ key: { type: 'string', description: 'The cache key to retrieve' },
3951
+ scope: {
3952
+ type: 'string',
3953
+ enum: ['workflow', 'global'],
3954
+ description: scopeDescription,
3955
+ },
3956
+ namespace: {
3957
+ type: 'string',
3958
+ description: `Namespace for isolation (default: "${defaultNamespace || 'default'}")`,
3959
+ },
3960
+ },
3961
+ required: ['key'],
3962
+ },
3963
+ },
3964
+ {
3965
+ name: 'memory_set',
3966
+ description: 'Store a value in cache with optional TTL (time-to-live in seconds).',
3967
+ toolType: 'function',
3968
+ parameters: {
3969
+ type: 'object',
3970
+ properties: {
3971
+ key: { type: 'string', description: 'The cache key' },
3972
+ value: { type: 'string', description: 'The value to cache (any JSON-serializable data)' },
3973
+ ttl: { type: 'number', description: 'Time-to-live in seconds (0 = no expiration)' },
3974
+ scope: {
3975
+ type: 'string',
3976
+ enum: ['workflow', 'global'],
3977
+ description: scopeDescription,
3978
+ },
3979
+ namespace: {
3980
+ type: 'string',
3981
+ description: `Namespace for isolation (default: "${defaultNamespace || 'default'}")`,
3982
+ },
3983
+ },
3984
+ required: ['key', 'value'],
3985
+ },
3986
+ },
3987
+ {
3988
+ name: 'memory_delete',
3989
+ description: 'Delete a cached value.',
3990
+ toolType: 'function',
3991
+ parameters: {
3992
+ type: 'object',
3993
+ properties: {
3994
+ key: { type: 'string', description: 'The cache key to delete' },
3995
+ scope: {
3996
+ type: 'string',
3997
+ enum: ['workflow', 'global'],
3998
+ description: scopeDescription,
3999
+ },
4000
+ namespace: {
4001
+ type: 'string',
4002
+ description: `Namespace for isolation (default: "${defaultNamespace || 'default'}")`,
4003
+ },
4004
+ },
4005
+ required: ['key'],
4006
+ },
4007
+ },
4008
+ {
4009
+ name: 'memory_list',
4010
+ description: 'List all cache keys in a namespace.',
4011
+ toolType: 'function',
4012
+ parameters: {
4013
+ type: 'object',
4014
+ properties: {
4015
+ scope: {
4016
+ type: 'string',
4017
+ enum: ['workflow', 'global'],
4018
+ description: scopeDescription,
4019
+ },
4020
+ namespace: {
4021
+ type: 'string',
4022
+ description: `Namespace for isolation (default: "${defaultNamespace || 'default'}")`,
4023
+ },
4024
+ },
4025
+ required: [],
4026
+ },
4027
+ },
4028
+ ];
4029
+ }
4030
+ case 'kv':
4031
+ default: {
4032
+ return [
4033
+ {
4034
+ name: 'memory_get',
4035
+ description: 'Retrieve a value from workflow or global memory. Use this to recall information stored in previous workflow executions.',
4036
+ toolType: 'function',
4037
+ parameters: {
4038
+ type: 'object',
4039
+ properties: {
4040
+ scope: {
4041
+ type: 'string',
4042
+ enum: ['workflow', 'global'],
4043
+ description: scopeDescription,
4044
+ },
4045
+ namespace: {
4046
+ type: 'string',
4047
+ description: `Namespace to organize related data (default: "${defaultNamespace || 'default'}")`,
4048
+ },
4049
+ key: { type: 'string', description: 'The key to retrieve' },
4050
+ },
4051
+ required: ['key'],
4052
+ },
4053
+ },
4054
+ {
4055
+ name: 'memory_set',
4056
+ description: 'Store a value in workflow or global memory for future executions.',
4057
+ toolType: 'function',
4058
+ parameters: {
4059
+ type: 'object',
4060
+ properties: {
4061
+ scope: {
4062
+ type: 'string',
4063
+ enum: ['workflow', 'global'],
4064
+ description: scopeDescription,
4065
+ },
4066
+ namespace: {
4067
+ type: 'string',
4068
+ description: `Namespace to organize related data (default: "${defaultNamespace || 'default'}")`,
4069
+ },
4070
+ key: { type: 'string', description: 'The key to store under' },
4071
+ value: { type: 'string', description: 'The value to store (any JSON-serializable data)' },
4072
+ },
4073
+ required: ['key', 'value'],
4074
+ },
4075
+ },
4076
+ {
4077
+ name: 'memory_delete',
4078
+ description: 'Delete a value from workflow or global memory.',
4079
+ toolType: 'function',
4080
+ parameters: {
4081
+ type: 'object',
4082
+ properties: {
4083
+ scope: {
4084
+ type: 'string',
4085
+ enum: ['workflow', 'global'],
4086
+ description: scopeDescription,
4087
+ },
4088
+ namespace: {
4089
+ type: 'string',
4090
+ description: `Namespace containing the key (default: "${defaultNamespace || 'default'}")`,
4091
+ },
4092
+ key: { type: 'string', description: 'The key to delete' },
4093
+ },
4094
+ required: ['key'],
4095
+ },
4096
+ },
4097
+ {
4098
+ name: 'memory_list',
4099
+ description: 'List all keys in a namespace for workflow or global memory.',
4100
+ toolType: 'function',
4101
+ parameters: {
4102
+ type: 'object',
4103
+ properties: {
4104
+ scope: {
4105
+ type: 'string',
4106
+ enum: ['workflow', 'global'],
4107
+ description: scopeDescription,
4108
+ },
4109
+ namespace: {
4110
+ type: 'string',
4111
+ description: `Namespace to list keys from (default: "${defaultNamespace || 'default'}")`,
4112
+ },
4113
+ },
4114
+ required: [],
4115
+ },
4116
+ },
4117
+ ];
4118
+ }
4119
+ }
4120
+ }
3643
4121
  async function executeAgentNode(node, context, options, trace, state, workflowFile, memoryBackend) {
3644
4122
  const data = node.data;
3645
4123
  // Check if workflow is in debug mode (from execution options)
@@ -3665,38 +4143,11 @@ async function executeAgentNode(node, context, options, trace, state, workflowFi
3665
4143
  const toolRouterNode = workflowFile.nodes.find(n => n.id === toolsEdge.target && n.type === 'tool-call-router');
3666
4144
  if (toolRouterNode) {
3667
4145
  connectedToolRouterNodeId = toolRouterNode.id;
3668
- // Find all Tool nodes that are children of this Tool Router
3669
- const childToolNodes = workflowFile.nodes.filter(n => n.parentId === toolRouterNode.id && n.type === 'tool');
3670
- // Convert Tool nodes to AgentTool format
4146
+ // Find all tool-like nodes that are children of this Tool Router
4147
+ const childToolNodes = workflowFile.nodes.filter(n => n.parentId === toolRouterNode.id && TOOL_CONTAINER_CHILD_TYPES.has(n.type || ''));
4148
+ // Convert tool nodes to AgentTool format
3671
4149
  for (const toolNode of childToolNodes) {
3672
- const toolData = toolNode.data;
3673
- // Map toolType: command -> function (we'll handle it specially during execution)
3674
- // Map toolType: code -> function (same handling)
3675
- const agentToolType = toolData.toolType === 'http' ? 'http' :
3676
- toolData.toolType === 'mcp' ? 'mcp' :
3677
- 'function'; // command, code, and function all map to 'function'
3678
- const agentTool = {
3679
- name: toolData.toolName,
3680
- description: toolData.description || `Tool: ${toolData.toolName}`,
3681
- toolType: agentToolType,
3682
- parameters: toolData.parameterSchema,
3683
- // Store original data for execution routing
3684
- httpConfig: toolData.toolType === 'http' ? {
3685
- method: toolData.httpMethod || 'GET',
3686
- url: toolData.httpUrl || '',
3687
- headers: toolData.httpHeaders,
3688
- bodyTemplate: toolData.httpBody,
3689
- } : undefined,
3690
- mcpConfig: toolData.toolType === 'mcp' ? {
3691
- serverUrl: toolData.mcpServerUrl,
3692
- serverName: toolData.mcpServerName,
3693
- } : undefined,
3694
- };
3695
- agentTool.
3696
- _toolNodeId = toolNode.id;
3697
- agentTool.
3698
- _originalToolType = toolData.toolType;
3699
- collectedTools.push(agentTool);
4150
+ collectedTools.push(nodeToAgentTool(toolNode));
3700
4151
  }
3701
4152
  addTraceEntry(trace, {
3702
4153
  type: 'debug_step',
@@ -3706,116 +4157,14 @@ async function executeAgentNode(node, context, options, trace, state, workflowFi
3706
4157
  message: `Collected ${childToolNodes.length} tools from Tool Router '${toolRouterNode.data.label}'`,
3707
4158
  data: {
3708
4159
  toolRouterNodeId: toolRouterNode.id,
3709
- collectedToolNames: childToolNodes.map(n => n.data.toolName),
4160
+ collectedToolNames: childToolNodes.map(n => n.data.toolName || n.data.label),
3710
4161
  },
3711
4162
  }, options);
3712
4163
  }
3713
4164
  }
3714
4165
  }
3715
- // Add memory tools to enable LLM access to workflow/global memory
3716
- const memoryTools = [
3717
- {
3718
- name: 'memory_get',
3719
- description: 'Retrieve a value from workflow or global memory. Use this to recall information stored in previous workflow executions.',
3720
- toolType: 'function',
3721
- parameters: {
3722
- type: 'object',
3723
- properties: {
3724
- scope: {
3725
- type: 'string',
3726
- enum: ['workflow', 'global'],
3727
- description: 'workflow: data persists across executions of this workflow; global: data shared across all workflows'
3728
- },
3729
- namespace: {
3730
- type: 'string',
3731
- description: 'Namespace to organize related data (e.g., "user_preferences", "session_data")'
3732
- },
3733
- key: {
3734
- type: 'string',
3735
- description: 'The key to retrieve'
3736
- }
3737
- },
3738
- required: ['scope', 'namespace', 'key']
3739
- }
3740
- },
3741
- {
3742
- name: 'memory_set',
3743
- description: 'Store a value in workflow or global memory for future executions. Use this to remember information across workflow runs.',
3744
- toolType: 'function',
3745
- parameters: {
3746
- type: 'object',
3747
- properties: {
3748
- scope: {
3749
- type: 'string',
3750
- enum: ['workflow', 'global'],
3751
- description: 'workflow: data persists across executions of this workflow; global: data shared across all workflows'
3752
- },
3753
- namespace: {
3754
- type: 'string',
3755
- description: 'Namespace to organize related data (e.g., "user_preferences", "session_data")'
3756
- },
3757
- key: {
3758
- type: 'string',
3759
- description: 'The key to store under'
3760
- },
3761
- value: {
3762
- type: 'string',
3763
- description: 'The value to store (any JSON-serializable data)'
3764
- },
3765
- ttl: {
3766
- type: 'number',
3767
- description: 'Optional: Time-to-live in seconds (for cache mode only)'
3768
- }
3769
- },
3770
- required: ['scope', 'namespace', 'key', 'value']
3771
- }
3772
- },
3773
- {
3774
- name: 'memory_delete',
3775
- description: 'Delete a value from workflow or global memory.',
3776
- toolType: 'function',
3777
- parameters: {
3778
- type: 'object',
3779
- properties: {
3780
- scope: {
3781
- type: 'string',
3782
- enum: ['workflow', 'global'],
3783
- description: 'workflow or global scope'
3784
- },
3785
- namespace: {
3786
- type: 'string',
3787
- description: 'Namespace containing the key'
3788
- },
3789
- key: {
3790
- type: 'string',
3791
- description: 'The key to delete'
3792
- }
3793
- },
3794
- required: ['scope', 'namespace', 'key']
3795
- }
3796
- },
3797
- {
3798
- name: 'memory_list',
3799
- description: 'List all keys in a namespace for workflow or global memory.',
3800
- toolType: 'function',
3801
- parameters: {
3802
- type: 'object',
3803
- properties: {
3804
- scope: {
3805
- type: 'string',
3806
- enum: ['workflow', 'global'],
3807
- description: 'workflow or global scope'
3808
- },
3809
- namespace: {
3810
- type: 'string',
3811
- description: 'Namespace to list keys from'
3812
- }
3813
- },
3814
- required: ['scope', 'namespace']
3815
- }
3816
- }
3817
- ];
3818
- // Add memory tools to collected tools
4166
+ // Add memory tools adapted to the docked memory node's mode (kv/conversation/cache)
4167
+ const memoryTools = buildMemoryTools(node.id, workflowFile);
3819
4168
  collectedTools.push(...memoryTools);
3820
4169
  // Resolve user prompt with template expressions
3821
4170
  let userPrompt = data.userPrompt || '{{ input }}';
@@ -3965,7 +4314,9 @@ async function executeAgentNode(node, context, options, trace, state, workflowFi
3965
4314
  }
3966
4315
  // Tool call found - execute it
3967
4316
  totalToolCalls++;
3968
- const toolName = toolCallResult.toolName;
4317
+ // Normalize tool name: strip 'functions.' prefix that some LLMs add (e.g. "functions.web_search" → "web_search")
4318
+ const rawToolName = toolCallResult.toolName;
4319
+ const toolName = rawToolName.startsWith('functions.') ? rawToolName.slice('functions.'.length) : rawToolName;
3969
4320
  const toolParams = toolCallResult.toolParameters || {};
3970
4321
  // Add assistant message with tool call to history
3971
4322
  conversationHistory.push({
@@ -4663,158 +5014,25 @@ Analyze the input above. Return a JSON object:
4663
5014
  }
4664
5015
  // Collect tools from child nodes or inline tools
4665
5016
  let collectedTools = [...(data.tools || [])];
4666
- // Find tool nodes that are children of this chat-agent node
5017
+ // Find tool-like nodes that are children of this chat-agent node
4667
5018
  if (workflowFile) {
4668
- const childToolNodes = workflowFile.nodes.filter(n => n.parentId === node.id && n.type === 'tool');
5019
+ const childToolNodes = workflowFile.nodes.filter(n => n.parentId === node.id && TOOL_CONTAINER_CHILD_TYPES.has(n.type || ''));
4669
5020
  for (const toolNode of childToolNodes) {
4670
- const toolData = toolNode.data;
4671
- const agentToolType = toolData.toolType === 'http' ? 'http' :
4672
- toolData.toolType === 'mcp' ? 'mcp' : 'function';
4673
- const agentTool = {
4674
- name: toolData.toolName,
4675
- description: toolData.description || `Tool: ${toolData.toolName}`,
4676
- toolType: agentToolType,
4677
- parameters: toolData.parameterSchema,
4678
- httpConfig: toolData.toolType === 'http' ? {
4679
- method: toolData.httpMethod || 'GET',
4680
- url: toolData.httpUrl || '',
4681
- headers: toolData.httpHeaders,
4682
- bodyTemplate: toolData.httpBody,
4683
- } : undefined,
4684
- mcpConfig: toolData.toolType === 'mcp' ? {
4685
- serverUrl: toolData.mcpServerUrl,
4686
- serverName: toolData.mcpServerName,
4687
- } : undefined,
4688
- };
4689
- agentTool._toolNodeId = toolNode.id;
4690
- agentTool._originalToolType = toolData.toolType;
4691
- collectedTools.push(agentTool);
5021
+ collectedTools.push(nodeToAgentTool(toolNode));
4692
5022
  }
4693
5023
  // Also check for connected tool-call-router
4694
5024
  if (data.toolRouterNodeId) {
4695
5025
  const toolRouterNode = workflowFile.nodes.find(n => n.id === data.toolRouterNodeId && n.type === 'tool-call-router');
4696
5026
  if (toolRouterNode) {
4697
- const routerChildTools = workflowFile.nodes.filter(n => n.parentId === toolRouterNode.id && n.type === 'tool');
5027
+ const routerChildTools = workflowFile.nodes.filter(n => n.parentId === toolRouterNode.id && TOOL_CONTAINER_CHILD_TYPES.has(n.type || ''));
4698
5028
  for (const toolNode of routerChildTools) {
4699
- const toolData = toolNode.data;
4700
- const agentToolType = toolData.toolType === 'http' ? 'http' :
4701
- toolData.toolType === 'mcp' ? 'mcp' : 'function';
4702
- const agentTool = {
4703
- name: toolData.toolName,
4704
- description: toolData.description || `Tool: ${toolData.toolName}`,
4705
- toolType: agentToolType,
4706
- parameters: toolData.parameterSchema,
4707
- };
4708
- agentTool._toolNodeId = toolNode.id;
4709
- collectedTools.push(agentTool);
5029
+ collectedTools.push(nodeToAgentTool(toolNode));
4710
5030
  }
4711
5031
  }
4712
5032
  }
4713
5033
  }
4714
- // Add memory tools to enable LLM access to workflow/global memory
4715
- const memoryTools = [
4716
- {
4717
- name: 'memory_get',
4718
- description: 'Retrieve a value from workflow or global memory. Use this to recall information stored in previous workflow executions.',
4719
- toolType: 'function',
4720
- parameters: {
4721
- type: 'object',
4722
- properties: {
4723
- scope: {
4724
- type: 'string',
4725
- enum: ['workflow', 'global'],
4726
- description: 'workflow: data persists across executions of this workflow; global: data shared across all workflows'
4727
- },
4728
- namespace: {
4729
- type: 'string',
4730
- description: 'Namespace to organize related data (e.g., "user_preferences", "session_data")'
4731
- },
4732
- key: {
4733
- type: 'string',
4734
- description: 'The key to retrieve'
4735
- }
4736
- },
4737
- required: ['scope', 'namespace', 'key']
4738
- }
4739
- },
4740
- {
4741
- name: 'memory_set',
4742
- description: 'Store a value in workflow or global memory for future executions. Use this to remember information across workflow runs.',
4743
- toolType: 'function',
4744
- parameters: {
4745
- type: 'object',
4746
- properties: {
4747
- scope: {
4748
- type: 'string',
4749
- enum: ['workflow', 'global'],
4750
- description: 'workflow: data persists across executions of this workflow; global: data shared across all workflows'
4751
- },
4752
- namespace: {
4753
- type: 'string',
4754
- description: 'Namespace to organize related data (e.g., "user_preferences", "session_data")'
4755
- },
4756
- key: {
4757
- type: 'string',
4758
- description: 'The key to store under'
4759
- },
4760
- value: {
4761
- type: 'string',
4762
- description: 'The value to store (any JSON-serializable data)'
4763
- },
4764
- ttl: {
4765
- type: 'number',
4766
- description: 'Optional: Time-to-live in seconds (for cache mode only)'
4767
- }
4768
- },
4769
- required: ['scope', 'namespace', 'key', 'value']
4770
- }
4771
- },
4772
- {
4773
- name: 'memory_delete',
4774
- description: 'Delete a value from workflow or global memory.',
4775
- toolType: 'function',
4776
- parameters: {
4777
- type: 'object',
4778
- properties: {
4779
- scope: {
4780
- type: 'string',
4781
- enum: ['workflow', 'global'],
4782
- description: 'workflow or global scope'
4783
- },
4784
- namespace: {
4785
- type: 'string',
4786
- description: 'Namespace containing the key'
4787
- },
4788
- key: {
4789
- type: 'string',
4790
- description: 'The key to delete'
4791
- }
4792
- },
4793
- required: ['scope', 'namespace', 'key']
4794
- }
4795
- },
4796
- {
4797
- name: 'memory_list',
4798
- description: 'List all keys in a namespace for workflow or global memory.',
4799
- toolType: 'function',
4800
- parameters: {
4801
- type: 'object',
4802
- properties: {
4803
- scope: {
4804
- type: 'string',
4805
- enum: ['workflow', 'global'],
4806
- description: 'workflow or global scope'
4807
- },
4808
- namespace: {
4809
- type: 'string',
4810
- description: 'Namespace to list keys from'
4811
- }
4812
- },
4813
- required: ['scope', 'namespace']
4814
- }
4815
- }
4816
- ];
4817
- // Add memory tools to collected tools
5034
+ // Add memory tools adapted to the docked memory node's mode (kv/conversation/cache)
5035
+ const memoryTools = buildMemoryTools(node.id, workflowFile);
4818
5036
  collectedTools.push(...memoryTools);
4819
5037
  // Debug: log collected tools
4820
5038
  console.log(`[ChatAgentNode ${node.id}] Collected ${collectedTools.length} tools:`, collectedTools.map(t => t.name));
@@ -5043,7 +5261,9 @@ Analyze the input above. Return a JSON object:
5043
5261
  }
5044
5262
  // Tool call found
5045
5263
  totalToolCalls++;
5046
- const toolName = toolCallResult.toolName;
5264
+ // Normalize tool name: strip 'functions.' prefix that some LLMs add
5265
+ const rawToolName = toolCallResult.toolName;
5266
+ const toolName = rawToolName.startsWith('functions.') ? rawToolName.slice('functions.'.length) : rawToolName;
5047
5267
  const toolParams = toolCallResult.toolParameters || {};
5048
5268
  // Log the tool call with parameters
5049
5269
  addTraceEntry(trace, {
@@ -5565,6 +5785,102 @@ async function executeAgentTool(tool, params, context, options, nodeId, trace, w
5565
5785
  const keys = await memoryBackend.list(scope, namespace);
5566
5786
  return { keys, count: keys.length };
5567
5787
  }
5788
+ // Conversation memory tools
5789
+ case 'memory_get_history': {
5790
+ const conversationId = params.conversation_id || 'default';
5791
+ const convKey = `__conv__${conversationId}`;
5792
+ const resolvedScope = scope || 'workflow';
5793
+ const resolvedNamespace = namespace || '';
5794
+ addTraceEntry(trace, {
5795
+ type: 'debug_step',
5796
+ nodeId,
5797
+ nodeName: tool.name,
5798
+ nodeType: 'agent',
5799
+ message: `Getting conversation history: ${resolvedScope}:${resolvedNamespace}:${convKey}`,
5800
+ data: { scope: resolvedScope, namespace: resolvedNamespace, conversationId },
5801
+ }, options);
5802
+ const stored = await memoryBackend.get(resolvedScope, resolvedNamespace, convKey);
5803
+ const messages = Array.isArray(stored) ? stored : [];
5804
+ return { messages, messageCount: messages.length, conversationId };
5805
+ }
5806
+ case 'memory_append': {
5807
+ const conversationId = params.conversation_id || 'default';
5808
+ const convKey = `__conv__${conversationId}`;
5809
+ const role = params.role;
5810
+ const content = params.content;
5811
+ const resolvedScope = scope || 'workflow';
5812
+ const resolvedNamespace = namespace || '';
5813
+ addTraceEntry(trace, {
5814
+ type: 'debug_step',
5815
+ nodeId,
5816
+ nodeName: tool.name,
5817
+ nodeType: 'agent',
5818
+ message: `Appending ${role} message to conversation ${conversationId}`,
5819
+ data: { scope: resolvedScope, namespace: resolvedNamespace, conversationId, role },
5820
+ }, options);
5821
+ // Load existing conversation
5822
+ const stored = await memoryBackend.get(resolvedScope, resolvedNamespace, convKey);
5823
+ const messages = Array.isArray(stored) ? stored : [];
5824
+ // Append new message
5825
+ const message = { role, content, timestamp: Date.now() };
5826
+ messages.push(message);
5827
+ // Apply max messages limit if configured via the docked memory node
5828
+ // The buildMemoryTools function embeds maxMessages in the tool description
5829
+ // but we also check the docked memory node directly
5830
+ if (workflowFile) {
5831
+ const dockedMemoryNode = workflowFile.nodes.find(n => {
5832
+ const nd = n.data;
5833
+ return n.type === 'memory' &&
5834
+ nd.dockedTo?.nodeId === nodeId &&
5835
+ nd.dockedTo?.handleId === 'memory';
5836
+ });
5837
+ if (dockedMemoryNode) {
5838
+ const memData = dockedMemoryNode.data;
5839
+ const maxMessages = memData.maxMessages ?? 0;
5840
+ if (maxMessages > 0) {
5841
+ const includeSystem = memData.includeSystemInWindow ?? true;
5842
+ if (includeSystem) {
5843
+ while (messages.length > maxMessages) {
5844
+ messages.shift();
5845
+ }
5846
+ }
5847
+ else {
5848
+ const systemMsgs = messages.filter(m => m.role === 'system');
5849
+ const nonSystemMsgs = messages.filter(m => m.role !== 'system');
5850
+ while (nonSystemMsgs.length > maxMessages) {
5851
+ nonSystemMsgs.shift();
5852
+ }
5853
+ // Rebuild preserving order
5854
+ messages.length = 0;
5855
+ messages.push(...systemMsgs, ...nonSystemMsgs);
5856
+ messages.sort((a, b) => a.timestamp - b.timestamp);
5857
+ }
5858
+ }
5859
+ }
5860
+ }
5861
+ // Store back
5862
+ await memoryBackend.set(resolvedScope, resolvedNamespace, convKey, messages);
5863
+ return { success: true, conversationId, messageCount: messages.length, appended: message };
5864
+ }
5865
+ case 'memory_clear_history': {
5866
+ const conversationId = params.conversation_id || 'default';
5867
+ const convKey = `__conv__${conversationId}`;
5868
+ const resolvedScope = scope || 'workflow';
5869
+ const resolvedNamespace = namespace || '';
5870
+ addTraceEntry(trace, {
5871
+ type: 'debug_step',
5872
+ nodeId,
5873
+ nodeName: tool.name,
5874
+ nodeType: 'agent',
5875
+ message: `Clearing conversation history: ${conversationId}`,
5876
+ data: { scope: resolvedScope, namespace: resolvedNamespace, conversationId },
5877
+ }, options);
5878
+ // Get count before clearing
5879
+ const stored = await memoryBackend.get(resolvedScope, resolvedNamespace, convKey);
5880
+ const count = Array.isArray(stored) ? stored.length : 0;
5881
+ await memoryBackend.delete(resolvedScope, resolvedNamespace, convKey);
5882
+ return { success: true, conversationId, clearedCount: count };
5883
+ }
5568
5884
  default:
5569
5885
  throw new Error(`Unknown memory tool: ${tool.name}`);
5570
5886
  }
@@ -5629,6 +5945,7 @@ async function executeAgentTool(tool, params, context, options, nodeId, trace, w
5629
5945
  ...tool.httpConfig.headers,
5630
5946
  },
5631
5947
  body,
5948
+ connectionId: tool._connectionId,
5632
5949
  },
5633
5950
  };
5634
5951
  const result = await options.onToolCall(toolCallRequest);
@@ -5729,6 +6046,91 @@ async function executeAgentTool(tool, params, context, options, nodeId, trace, w
5729
6046
  }
5730
6047
  return applyOutputTransform(result.result, tool._toolNodeId);
5731
6048
  }
6049
+ case 'command':
6050
+ case 'web-search':
6051
+ case 'database-query': {
6052
+ // Route through onToolCall callback for Electron-side execution
6053
+ if (!options.onToolCall) {
6054
+ throw new Error(`Tool '${tool.name}' (type: ${tool.toolType}) requires onToolCall callback`);
6055
+ }
6056
+ addTraceEntry(trace, {
6057
+ type: 'debug_step',
6058
+ nodeId,
6059
+ nodeName: tool.name,
6060
+ nodeType: 'agent',
6061
+ message: `Executing ${tool.toolType} tool '${tool.name}' via onToolCall`,
6062
+ data: { toolType: tool.toolType, params },
6063
+ }, options);
6064
+ const toolCallRequest = {
6065
+ nodeId,
6066
+ toolName: tool.name,
6067
+ toolType: tool.toolType,
6068
+ parameters: params,
6069
+ };
6070
+ // Add type-specific config from the source tool node
6071
+ if (tool._toolNodeId && workflowFile) {
6072
+ const toolNode = workflowFile.nodes.find(n => n.id === tool._toolNodeId);
6073
+ if (toolNode) {
6074
+ if (tool.toolType === 'command') {
6075
+ const toolData = toolNode.data;
6076
+ let commandArgs = toolData.commandArgs || '';
6077
+ for (const [key, value] of Object.entries(params)) {
6078
+ commandArgs = commandArgs.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), String(value));
6079
+ }
6080
+ toolCallRequest.commandConfig = {
6081
+ executable: toolData.commandExecutable || '',
6082
+ args: commandArgs,
6083
+ cwd: toolData.commandCwd,
6084
+ requiresApproval: toolData.commandRequiresApproval,
6085
+ };
6086
+ }
6087
+ else if (tool.toolType === 'web-search') {
6088
+ const toolData = toolNode.data;
6089
+ const baseData = toolNode.data;
6090
+ // LLM sends query via params (e.g. { query: "...", input: "..." })
6091
+ const searchQuery = params.query || params.input || params.search_query || '';
6092
+ toolCallRequest.webSearchConfig = {
6093
+ query: searchQuery,
6094
+ resultCount: toolData.resultCount || 5,
6095
+ // Pass inline config if set, otherwise pass connectionId for resolution
6096
+ connectionConfig: toolData.provider ? {
6097
+ provider: toolData.provider,
6098
+ apiKey: toolData.apiKey,
6099
+ instanceUrl: toolData.instanceUrl,
6100
+ } : undefined,
6101
+ connectionId: baseData.connectionId,
6102
+ };
6103
+ }
6104
+ else if (tool.toolType === 'database-query') {
6105
+ const toolData = toolNode.data;
6106
+ // LLM may override query via params, otherwise use node's configured query
6107
+ const query = params.query || toolData.query || '';
6108
+ toolCallRequest.databaseConfig = {
6109
+ connectionId: toolData.connectionId,
6110
+ queryType: toolData.queryType || 'select',
6111
+ query,
6112
+ parameters: params.parameters || toolData.parameters,
6113
+ collection: params.collection || toolData.collection,
6114
+ maxRows: toolData.maxRows,
6115
+ timeoutMs: toolData.timeoutMs,
6116
+ };
6117
+ }
6118
+ }
6119
+ }
6120
+ const result = await options.onToolCall(toolCallRequest);
6121
+ if (!result.success) {
6122
+ throw new Error(result.error || `Tool '${tool.name}' execution failed`);
6123
+ }
6124
+ addTraceEntry(trace, {
6125
+ type: 'debug_step',
6126
+ nodeId,
6127
+ nodeName: tool.name,
6128
+ nodeType: 'agent',
6129
+ message: `${tool.toolType} tool '${tool.name}' completed`,
6130
+ data: { success: result.success, output: result.result },
6131
+ }, options);
6132
+ return applyOutputTransform(result.result, tool._toolNodeId);
6133
+ }
5732
6134
  case 'workflow': {
5733
6135
  // Workflow tools are not yet supported in agent context
5734
6136
  throw new Error(`Workflow tools are not yet supported in agent nodes. Tool: ${tool.name}`);