@illuma-ai/agents 1.1.22 → 1.1.24

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.
@@ -2031,6 +2031,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2031
2031
  );
2032
2032
  }
2033
2033
 
2034
+
2034
2035
  // Get model info for analytics
2035
2036
  const bedrockOpts = agentContext.clientOptions as
2036
2037
  | t.BedrockAnthropicClientOptions
@@ -678,6 +678,7 @@ export class MultiAgentGraph extends StandardGraph {
678
678
  ];
679
679
  }
680
680
 
681
+
681
682
  const childState: t.BaseGraphState = {
682
683
  messages: childMessages,
683
684
  };
@@ -688,6 +689,25 @@ export class MultiAgentGraph extends StandardGraph {
688
689
  );
689
690
 
690
691
  try {
692
+ /**
693
+ * Dispatch transition BEFORE invoking the child subgraph so that
694
+ * callbacks.js sets multiAgentTrace.isMultiAgent = true before the
695
+ * child's ON_RUN_STEP events fire. This ensures child tool calls
696
+ * are attributed to the correct agent in admin traces.
697
+ */
698
+ await safeDispatchCustomEvent(
699
+ GraphEvents.ON_AGENT_TRANSITION,
700
+ {
701
+ sourceAgentId: sourceAgentId,
702
+ sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
703
+ destinationAgentId: destination,
704
+ destinationAgentName: destContext?.name ?? destination,
705
+ edgeType: EdgeType.HANDOFF,
706
+ timestamp: Date.now(),
707
+ },
708
+ config
709
+ );
710
+
691
711
  /**
692
712
  * Invoke the child subgraph with config propagation.
693
713
  * Config carries callbacks (for SSE streaming), abort signal,
@@ -710,19 +730,6 @@ export class MultiAgentGraph extends StandardGraph {
710
730
  `${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`
711
731
  );
712
732
 
713
- await safeDispatchCustomEvent(
714
- GraphEvents.ON_AGENT_TRANSITION,
715
- {
716
- sourceAgentId: sourceAgentId,
717
- sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
718
- destinationAgentId: destination,
719
- destinationAgentName: destContext?.name ?? destination,
720
- edgeType: EdgeType.HANDOFF,
721
- timestamp: Date.now(),
722
- },
723
- config
724
- );
725
-
726
733
  return truncatedResult;
727
734
  } catch (err) {
728
735
  const errorMessage =
@@ -970,6 +977,28 @@ export class MultiAgentGraph extends StandardGraph {
970
977
  }
971
978
  }
972
979
 
980
+ /**
981
+ * Ensure messages end with a HumanMessage.
982
+ * After stripping tool artifacts, the last message may be an AIMessage
983
+ * (orchestrator's reasoning before the handoff). Some providers (Bedrock,
984
+ * Google/VertexAI) reject conversations ending with an assistant message.
985
+ * Convert the trailing AIMessage to a HumanMessage to preserve any useful
986
+ * context (e.g., compacted tool summaries) while satisfying the API requirement.
987
+ */
988
+ if (cleaned.length > 0 && cleaned[cleaned.length - 1].getType() === 'ai') {
989
+ const lastAI = cleaned[cleaned.length - 1];
990
+ const content = typeof lastAI.content === 'string'
991
+ ? lastAI.content
992
+ : '';
993
+ if (content.trim()) {
994
+ cleaned[cleaned.length - 1] = new HumanMessage(
995
+ `[Context from orchestrator]: ${content}`
996
+ );
997
+ } else {
998
+ cleaned.pop();
999
+ }
1000
+ }
1001
+
973
1002
  return cleaned;
974
1003
  }
975
1004
 
@@ -215,20 +215,35 @@ describe('prepareHandoffMessages', () => {
215
215
  expect(MultiAgentGraph.prepareHandoffMessages([])).toEqual([]);
216
216
  });
217
217
 
218
- it('passes through plain HumanMessage and text-only AIMessage unchanged', () => {
218
+ it('converts trailing AIMessage to HumanMessage context', () => {
219
219
  const messages: BaseMessage[] = [
220
220
  new HumanMessage('Hello'),
221
221
  new AIMessage('Hi there'),
222
222
  ];
223
223
  const result = MultiAgentGraph.prepareHandoffMessages(messages);
224
+ // Trailing AI converted to HumanMessage so conversation ends with user message
224
225
  expect(result).toHaveLength(2);
225
226
  expect(result[0].getType()).toBe('human');
226
227
  expect(result[0].content).toBe('Hello');
228
+ expect(result[1].getType()).toBe('human');
229
+ expect(result[1].content).toContain('Hi there');
230
+ });
231
+
232
+ it('preserves AI messages that are not trailing', () => {
233
+ const messages: BaseMessage[] = [
234
+ new HumanMessage('Hello'),
235
+ new AIMessage('Hi there'),
236
+ new HumanMessage('Follow up'),
237
+ ];
238
+ const result = MultiAgentGraph.prepareHandoffMessages(messages);
239
+ expect(result).toHaveLength(3);
240
+ expect(result[0].content).toBe('Hello');
227
241
  expect(result[1].getType()).toBe('ai');
228
242
  expect(result[1].content).toBe('Hi there');
243
+ expect(result[2].content).toBe('Follow up');
229
244
  });
230
245
 
231
- it('strips orphaned tool_use blocks (no matching tool_result)', () => {
246
+ it('strips orphaned tool_use, converts trailing AI to context', () => {
232
247
  const messages: BaseMessage[] = [
233
248
  new HumanMessage('Do something'),
234
249
  new AIMessage({
@@ -242,12 +257,9 @@ describe('prepareHandoffMessages', () => {
242
257
  const result = MultiAgentGraph.prepareHandoffMessages(messages);
243
258
  expect(result).toHaveLength(2);
244
259
  expect(result[0].content).toBe('Do something');
245
- // AI message should be converted to text-only (no tool_calls)
246
- expect(result[1].getType()).toBe('ai');
260
+ // Trailing AI converted to HumanMessage context
261
+ expect(result[1].getType()).toBe('human');
247
262
  expect(result[1].content).toContain('Let me call a tool');
248
- // Should NOT contain tool_calls
249
- const aiMsg = result[1] as AIMessage;
250
- expect(aiMsg.tool_calls ?? []).toHaveLength(0);
251
263
  });
252
264
 
253
265
  it('compacts paired tool_use/tool_result into text summaries', () => {
@@ -264,17 +276,20 @@ describe('prepareHandoffMessages', () => {
264
276
  ];
265
277
  const result = MultiAgentGraph.prepareHandoffMessages(messages);
266
278
 
267
- // Should have: HumanMessage, compacted AIMessage (with tool summary), final AIMessage
279
+ // HumanMessage + compacted AI + trailing AI converted to HumanMessage
268
280
  expect(result).toHaveLength(3);
269
281
 
270
282
  // ToolMessage should be removed
271
283
  expect(result.every((m) => m.getType() !== 'tool')).toBe(true);
272
284
 
273
285
  // Compacted AI message should contain tool summary
274
- const compactedAI = result[1];
275
- expect(compactedAI.content).toContain('I will search for you');
276
- expect(compactedAI.content).toContain('web_search');
277
- expect(compactedAI.content).toContain('Found 3 results');
286
+ expect(result[1].content).toContain('I will search for you');
287
+ expect(result[1].content).toContain('web_search');
288
+ expect(result[1].content).toContain('Found 3 results');
289
+
290
+ // Trailing AI converted to HumanMessage
291
+ expect(result[2].getType()).toBe('human');
292
+ expect(result[2].content).toContain('Based on the search');
278
293
  });
279
294
 
280
295
  it('removes ALL ToolMessages from output', () => {
@@ -326,25 +341,20 @@ describe('prepareHandoffMessages', () => {
326
341
  // No ToolMessages
327
342
  expect(result.filter((m) => m.getType() === 'tool')).toHaveLength(0);
328
343
 
329
- // No tool_calls on any AI message
330
- for (const msg of result) {
331
- if (msg.getType() === 'ai') {
332
- expect((msg as AIMessage).tool_calls ?? []).toHaveLength(0);
333
- }
334
- }
335
-
336
- // Should have: HumanMessage + 2 compacted/cleaned AI messages
344
+ // Should have: HumanMessage + compacted AI (with tool summary) + trailing AI converted to HumanMessage
337
345
  expect(result).toHaveLength(3);
338
346
 
339
347
  // First AI message should have researcher tool summary (paired)
348
+ expect(result[1].getType()).toBe('ai');
340
349
  expect(result[1].content).toContain('lc_handoff_to_researcher');
341
350
  expect(result[1].content).toContain('AI adoption');
342
351
 
343
- // Second AI message should be text-only (orphaned tool_use stripped)
352
+ // Trailing AI converted to HumanMessage context
353
+ expect(result[2].getType()).toBe('human');
344
354
  expect(result[2].content).toContain('Now I will delegate to the writer');
345
355
  });
346
356
 
347
- it('preserves message order', () => {
357
+ it('preserves message order, converts only trailing AI', () => {
348
358
  const messages: BaseMessage[] = [
349
359
  new HumanMessage('Step 1'),
350
360
  new AIMessage('Response 1'),
@@ -355,8 +365,10 @@ describe('prepareHandoffMessages', () => {
355
365
  expect(result).toHaveLength(4);
356
366
  expect(result[0].content).toBe('Step 1');
357
367
  expect(result[1].content).toBe('Response 1');
368
+ expect(result[1].getType()).toBe('ai'); // Not trailing — preserved as AI
358
369
  expect(result[2].content).toBe('Step 2');
359
- expect(result[3].content).toBe('Response 2');
370
+ expect(result[3].getType()).toBe('human'); // Trailing — converted
371
+ expect(result[3].content).toContain('Response 2');
360
372
  });
361
373
 
362
374
  it('drops AI messages that have only tool_calls and no text content', () => {
@@ -390,10 +402,12 @@ describe('prepareHandoffMessages', () => {
390
402
  new ToolMessage({ content: 'tool output', tool_call_id: 'tc1', name: 'some_tool' }),
391
403
  ];
392
404
  const result = MultiAgentGraph.prepareHandoffMessages(messages);
405
+ // Compacted AI is trailing → converted to HumanMessage
393
406
  expect(result).toHaveLength(2);
394
- const aiContent = result[1].content as string;
395
- expect(aiContent).toContain('Part one.');
396
- expect(aiContent).toContain('Part two.');
397
- expect(aiContent).toContain('some_tool');
407
+ expect(result[1].getType()).toBe('human');
408
+ const content = result[1].content as string;
409
+ expect(content).toContain('Part one.');
410
+ expect(content).toContain('Part two.');
411
+ expect(content).toContain('some_tool');
398
412
  });
399
413
  });
package/src/utils/llm.ts CHANGED
@@ -24,3 +24,4 @@ export function isGoogleLike(provider?: string | Providers): boolean {
24
24
  provider
25
25
  );
26
26
  }
27
+