@minded-ai/mindedjs 1.0.150 → 1.0.151-beta-1

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.
@@ -50,150 +50,334 @@ export const addPromptNode = async ({ graph, node, llm, tools, emit, agent }: Ad
50
50
  const systemMessage = combinedPlaybooks + '\n\n' + currentPromptNode;
51
51
  const compiledPrompt = compilePrompt(systemMessage, { memory: state.memory, system: { currentTime: new Date().toISOString() } });
52
52
 
53
- const startTime = Date.now();
54
- const result: AIMessage = await llmToUse.bindTools(scopedTools).invoke([new SystemMessage(compiledPrompt), ...state.messages]);
55
- await agent.interruptSessionManager.checkQueueAndInterrupt(state.sessionId);
56
- const endTime = Date.now();
57
- logger.debug({
58
- msg: '[Model] Model execution time',
59
- executionTimeMs: endTime - startTime,
60
- sessionId: state.sessionId,
61
- node: node.displayName,
62
- });
63
- // Check if the result contains tool calls
64
- if (result.tool_calls && result.tool_calls.length > 0) {
65
- // Execute the tools
66
- const toolResults = [];
67
- let stateUpdates = {};
68
-
69
- for (const toolCall of result.tool_calls) {
70
- const matchedTool = scopedTools.find((t) => t.name === toolCall.name);
71
- logger.info({
72
- msg: `[Model] Calling tool inside prompt node`,
73
- tool: matchedTool?.name,
53
+ // Check if we should use the agentic loop (only when humanInTheLoop is true)
54
+ const useAgenticLoop = node.humanInTheLoop === true;
55
+
56
+ /**
57
+ * When humanInTheLoop is true: Run an agentic loop that continues invoking the LLM
58
+ * after tool calls until we get a final AI message without tool calls.
59
+ *
60
+ * When humanInTheLoop is false: Return immediately after tool execution without
61
+ * additional LLM invocations.
62
+ */
63
+ let allMessages: any[] = [];
64
+ let allHistory: any[] = [];
65
+ let accumulatedStateUpdates = {};
66
+ let currentMessages = [...state.messages];
67
+ let finalAIMessage: AIMessage | null = null;
68
+ let loopCount = 0;
69
+ const MAX_LOOP_ITERATIONS = 10;
70
+
71
+ while (true) {
72
+ loopCount++;
73
+
74
+ // Break if we've reached the maximum number of iterations
75
+ if (loopCount > MAX_LOOP_ITERATIONS) {
76
+ logger.warn({
77
+ msg: '[Model] Reached maximum loop iterations, breaking agentic loop',
78
+ loopCount,
74
79
  sessionId: state.sessionId,
75
80
  node: node.displayName,
76
81
  });
77
- if (matchedTool) {
78
- try {
79
- // Invoke the LangChain tool directly
80
- const startTime = Date.now();
81
- const toolResult = await matchedTool.invoke(toolCall);
82
- const endTime = Date.now();
83
- logger.debug({
84
- msg: `[Tool] Tool result inside prompt node`,
85
- tool: matchedTool?.name,
86
- result: toolResult,
87
- executionTimeMs: endTime - startTime,
88
- sessionId: state.sessionId,
89
- node: node.displayName,
90
- });
91
- //check for queue after tool call
92
- const systemMessageId = uuidv4();
93
-
94
- await agent.interruptSessionManager.checkQueueAndInterrupt(state.sessionId, {
95
- messages: [
96
- result,
97
- toolResult,
98
- new SystemMessage({
99
- id: systemMessageId,
100
- content:
101
- 'you called tool when the user send a new message, Consider calling the function again after user message is processed',
102
- }),
103
- ],
104
- history: [
82
+ break;
83
+ }
84
+ const startTime = Date.now();
85
+
86
+ const result: AIMessage = await llmToUse.bindTools(scopedTools).invoke([new SystemMessage(compiledPrompt), ...currentMessages]);
87
+
88
+ // Always pass accumulated state to interrupt manager
89
+ // Pass the accumulated messages from this prompt node execution
90
+ // If empty, pass undefined to avoid empty state updates
91
+ const interruptState =
92
+ allMessages.length > 0 || allHistory.length > 0
93
+ ? {
94
+ messages: allMessages,
95
+ history: allHistory,
96
+ }
97
+ : undefined;
98
+ await agent.interruptSessionManager.checkQueueAndInterrupt(state.sessionId, interruptState);
99
+
100
+ const endTime = Date.now();
101
+
102
+ logger.debug({
103
+ msg: '[Model] Model execution time',
104
+ executionTimeMs: endTime - startTime,
105
+ sessionId: state.sessionId,
106
+ node: node.displayName,
107
+ });
108
+
109
+ // Check if the result contains tool calls
110
+ if (result.tool_calls && result.tool_calls.length > 0) {
111
+ // Add the tool call message
112
+ allMessages.push(result);
113
+ allHistory.push(
114
+ createHistoryStep<HistoryStep>(state.history, {
115
+ type: NodeType.TOOL,
116
+ nodeId: node.name,
117
+ nodeDisplayName: node.displayName,
118
+ raw: result,
119
+ messageIds: [result.id!],
120
+ }),
121
+ );
122
+
123
+ // Execute the tools
124
+ // Track how many tool responses we've added for validation
125
+ const toolResponseStartIndex = allMessages.length;
126
+
127
+ // Ensure we process ALL tool calls, even if some fail
128
+ for (const toolCall of result.tool_calls) {
129
+ const matchedTool = scopedTools.find((t) => t.name === toolCall.name);
130
+ logger.info({
131
+ msg: `[Model] Calling tool inside prompt node`,
132
+ tool: matchedTool?.name || toolCall.name,
133
+ toolCallId: toolCall.id,
134
+ sessionId: state.sessionId,
135
+ node: node.displayName,
136
+ });
137
+
138
+ if (matchedTool) {
139
+ try {
140
+ // Invoke the LangChain tool directly
141
+ const toolStartTime = Date.now();
142
+ const toolResult = await matchedTool.invoke(toolCall);
143
+ const toolEndTime = Date.now();
144
+
145
+ logger.debug({
146
+ msg: `[Tool] Tool result inside prompt node`,
147
+ tool: matchedTool?.name,
148
+ result: toolResult,
149
+ executionTimeMs: toolEndTime - toolStartTime,
150
+ sessionId: state.sessionId,
151
+ node: node.displayName,
152
+ });
153
+
154
+ // Add the tool result directly to allMessages
155
+ allMessages.push(toolResult);
156
+
157
+ // Check for queue after tool call
158
+ const systemMessageId = uuidv4();
159
+ await agent.interruptSessionManager.checkQueueAndInterrupt(state.sessionId, {
160
+ messages: [
161
+ ...allMessages,
162
+ new SystemMessage({
163
+ id: systemMessageId,
164
+ content:
165
+ 'you called tool when the user send a new message, Consider calling the function again after user message is processed',
166
+ }),
167
+ ],
168
+ history: [
169
+ ...allHistory,
170
+ createHistoryStep<HistoryStep>(state.history, {
171
+ type: NodeType.TOOL,
172
+ nodeId: node.name,
173
+ nodeDisplayName: node.displayName,
174
+ raw: toolResult,
175
+ messageIds: [toolResult.id!, systemMessageId],
176
+ }),
177
+ ],
178
+ });
179
+
180
+ const toolStateUpdate = extractToolStateResponse(toolResult);
181
+ // Properly merge memory and other state updates
182
+ accumulatedStateUpdates = {
183
+ ...accumulatedStateUpdates,
184
+ ...toolStateUpdate,
185
+ memory: { ...(accumulatedStateUpdates as any).memory, ...(toolStateUpdate as any).memory },
186
+ };
187
+
188
+ allHistory.push(
105
189
  createHistoryStep<HistoryStep>(state.history, {
106
190
  type: NodeType.TOOL,
107
191
  nodeId: node.name,
108
192
  nodeDisplayName: node.displayName,
109
193
  raw: toolResult,
110
- messageIds: [toolResult.id!, systemMessageId],
194
+ messageIds: [toolResult.id!],
111
195
  }),
112
- ],
113
- });
114
- const toolStateUpdate = extractToolStateResponse(toolResult);
115
- // Properly merge memory and other state updates
116
- stateUpdates = {
117
- ...stateUpdates,
118
- ...toolStateUpdate,
119
- memory: { ...(stateUpdates as any).memory, ...(toolStateUpdate as any).memory },
120
- };
121
- toolResults.push(toolResult);
122
- } catch (err: any) {
123
- if (err?.name === 'GraphInterrupt') throw err;
196
+ );
197
+ } catch (err: any) {
198
+ if (err?.name === 'GraphInterrupt') throw err;
199
+ logger.error({
200
+ msg: `[Tool] Error executing tool inside prompt node`,
201
+ tool: toolCall.name,
202
+ err,
203
+ sessionId: state.sessionId,
204
+ node: node.displayName,
205
+ });
206
+ const errorMessage = new ToolMessage({
207
+ content: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),
208
+ tool_call_id: toolCall.id!,
209
+ });
210
+
211
+ // Add the error message directly to allMessages
212
+ allMessages.push(errorMessage);
213
+
214
+ // Check for queue after tool error
215
+ const errorSystemMessageId = uuidv4();
216
+ await agent.interruptSessionManager.checkQueueAndInterrupt(state.sessionId, {
217
+ messages: [
218
+ ...allMessages,
219
+ new SystemMessage({
220
+ id: errorSystemMessageId,
221
+ content: 'Tool execution failed. Consider handling the error or calling another function if needed.',
222
+ }),
223
+ ],
224
+ history: [
225
+ ...allHistory,
226
+ createHistoryStep<HistoryStep>(state.history, {
227
+ type: NodeType.TOOL,
228
+ nodeId: node.name,
229
+ nodeDisplayName: node.displayName,
230
+ raw: errorMessage,
231
+ messageIds: [errorMessage.id!, errorSystemMessageId],
232
+ }),
233
+ ],
234
+ });
235
+ allHistory.push(
236
+ createHistoryStep<HistoryStep>(state.history, {
237
+ type: NodeType.TOOL,
238
+ nodeId: node.name,
239
+ nodeDisplayName: node.displayName,
240
+ raw: errorMessage,
241
+ messageIds: [errorMessage.id!],
242
+ }),
243
+ );
244
+ }
245
+ } else {
124
246
  logger.error({
125
- msg: `[Tool] Error executing tool inside prompt node`,
247
+ msg: `[Tool] Model called tool but it was not found inside prompt node`,
126
248
  tool: toolCall.name,
127
- err,
249
+ toolCallId: toolCall.id,
128
250
  sessionId: state.sessionId,
129
251
  node: node.displayName,
130
252
  });
131
- const errorMessage = new ToolMessage({
132
- content: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),
133
- tool_call_id: toolCall.id!,
253
+
254
+ // IMPORTANT: Create an error message for the missing tool with the correct tool_call_id
255
+ const missingToolMessage = new ToolMessage({
256
+ content: JSON.stringify({ error: `Tool '${toolCall.name}' not found` }),
257
+ tool_call_id: toolCall.id!, // This MUST match the tool_call_id from the AI message
258
+ name: toolCall.name,
134
259
  });
135
- toolResults.push(errorMessage);
260
+
261
+ allMessages.push(missingToolMessage);
262
+ allHistory.push(
263
+ createHistoryStep<HistoryStep>(state.history, {
264
+ type: NodeType.TOOL,
265
+ nodeId: node.name,
266
+ nodeDisplayName: node.displayName,
267
+ raw: missingToolMessage,
268
+ messageIds: [missingToolMessage.id!],
269
+ }),
270
+ );
136
271
  }
137
- } else {
272
+ }
273
+
274
+ // Validate that we have responses for ALL tool calls
275
+ const toolResponseCount = allMessages.length - toolResponseStartIndex;
276
+ if (toolResponseCount !== result.tool_calls.length) {
138
277
  logger.error({
139
- msg: `[Tool] Model called tool but it was not found inside prompt node`,
140
- tool: toolCall.name,
278
+ msg: '[Model] Tool response count mismatch',
279
+ expectedCount: result.tool_calls.length,
280
+ actualCount: toolResponseCount,
281
+ toolCallIds: result.tool_calls.map((tc) => tc.id),
141
282
  sessionId: state.sessionId,
142
283
  node: node.displayName,
143
284
  });
144
285
  }
286
+
287
+ // If humanInTheLoop is false, return immediately after tool execution
288
+ if (!useAgenticLoop) {
289
+ return {
290
+ ...accumulatedStateUpdates,
291
+ messages: allMessages,
292
+ history: allHistory,
293
+ };
294
+ }
295
+
296
+ // Otherwise, prepare for next iteration
297
+ // allMessages now contains all the new messages from this iteration
298
+ currentMessages = [...state.messages, ...allMessages];
299
+
300
+ // Continue the loop to see what the model wants to do next
301
+ continue;
302
+ } else {
303
+ // We got an AI message without tool calls - this is our final response
304
+ finalAIMessage = result;
305
+ break;
145
306
  }
307
+ }
308
+
309
+ // Process the final AI message if we have one
310
+ if (finalAIMessage) {
311
+ // Update state so AI_MESSAGE will get the latest state
312
+ const stateForAIMessage = structuredClone(state);
313
+ stateForAIMessage.goto = null;
314
+ stateForAIMessage.messages.push(...allMessages, finalAIMessage);
315
+
316
+ let aiMessageStateUpdate: any = { goto: null };
317
+ let messagesToReturn = [...allMessages, finalAIMessage];
318
+
319
+ if (finalAIMessage.getType() === 'ai') {
320
+ logger.info({
321
+ msg: `[Model] Response`,
322
+ content: finalAIMessage.content,
323
+ sessionId: state.sessionId,
324
+ node: node.displayName,
325
+ });
326
+
327
+ const results = await emit(AgentEvents.AI_MESSAGE, {
328
+ message: finalAIMessage.content as string,
329
+ state: stateForAIMessage,
330
+ });
331
+
332
+ const handlerResult = results.find((r) => r !== undefined);
333
+ if (handlerResult && handlerResult.state) {
334
+ aiMessageStateUpdate = { ...aiMessageStateUpdate, ...handlerResult.state };
146
335
 
147
- // Return the tool call message and tool results with state updates spread at top level
336
+ // If the handler returned modified messages, use those instead
337
+ if (handlerResult.state.messages) {
338
+ messagesToReturn = [...allMessages, ...handlerResult.state.messages];
339
+ }
340
+ }
341
+ }
342
+
343
+ allHistory.push(
344
+ createHistoryStep<HistoryStep>(state.history, {
345
+ type: NodeType.PROMPT_NODE,
346
+ nodeId: node.name,
347
+ nodeDisplayName: node.displayName,
348
+ raw: finalAIMessage.content,
349
+ messageIds: [finalAIMessage.id!],
350
+ }),
351
+ );
352
+
353
+ // Return all accumulated state updates and messages
148
354
  return {
149
- ...stateUpdates,
150
- messages: [result, ...toolResults],
151
- history: [
152
- createHistoryStep<HistoryStep>(state.history, {
153
- type: NodeType.TOOL,
154
- nodeId: node.name,
155
- nodeDisplayName: node.displayName,
156
- raw: result,
157
- messageIds: [result.id!],
158
- }),
159
- ...toolResults.map((toolResult) =>
160
- createHistoryStep<HistoryStep>(state.history, {
161
- type: NodeType.TOOL,
162
- nodeId: node.name,
163
- nodeDisplayName: node.displayName,
164
- raw: toolResult,
165
- messageIds: [toolResult.id!],
166
- }),
167
- ),
168
- ],
355
+ ...accumulatedStateUpdates,
356
+ ...aiMessageStateUpdate,
357
+ messages: messagesToReturn,
358
+ history: allHistory,
169
359
  };
170
360
  }
171
361
 
172
- // Model text response
173
- state.goto = null;
174
- //update state so AI_MESSAGE will get the latest state
175
- const stateForAIMessage = structuredClone(state);
176
- stateForAIMessage.goto = null;
177
- stateForAIMessage.messages.push(result);
178
- let stateUpdate: any = { goto: null, messages: [result] };
179
- if (result.getType() === 'ai') {
180
- logger.info({ msg: `[Model] Response`, content: result.content, sessionId: state.sessionId, node: node.displayName });
181
- const results = await emit(AgentEvents.AI_MESSAGE, { message: result.content as string, state: stateForAIMessage });
182
- const handlerResult = results.find((r) => r !== undefined);
183
- if (handlerResult && handlerResult.state) {
184
- stateUpdate = { ...stateUpdate, ...handlerResult.state };
185
- }
362
+ // If we hit the loop limit without a final AI message, return what we have
363
+ if (loopCount > MAX_LOOP_ITERATIONS && allMessages.length > 0) {
364
+ logger.warn({
365
+ msg: '[Model] Returning accumulated messages after hitting loop limit',
366
+ messageCount: allMessages.length,
367
+ sessionId: state.sessionId,
368
+ node: node.displayName,
369
+ });
370
+
371
+ return {
372
+ ...accumulatedStateUpdates,
373
+ goto: null,
374
+ messages: allMessages,
375
+ history: allHistory,
376
+ };
186
377
  }
187
- return {
188
- ...stateUpdate,
189
- history: createHistoryStep<HistoryStep>(state.history, {
190
- type: NodeType.PROMPT_NODE,
191
- nodeId: node.name,
192
- nodeDisplayName: node.displayName,
193
- raw: result.content,
194
- messageIds: [result.id!],
195
- }),
196
- };
378
+
379
+ // This should only be reached in unexpected cases
380
+ throw new Error('Unexpected state: no messages generated in prompt node');
197
381
  };
198
382
  graph.addNode(node.name, callback);
199
383
  };