@illuma-ai/agents 1.1.14 → 1.1.15

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.
@@ -78,7 +78,9 @@ export declare enum EdgeType {
78
78
  /** Creates handoff tools for dynamic agent routing (default for single-to-single edges) */
79
79
  HANDOFF = "handoff",
80
80
  /** Creates direct edges for automatic sequential/parallel transitions */
81
- DIRECT = "direct"
81
+ DIRECT = "direct",
82
+ /** Creates delegate tools that invoke child subgraph inline and return result to parent */
83
+ DELEGATE = "delegate"
82
84
  }
83
85
  export declare enum GraphNodeKeys {
84
86
  TOOLS = "tools=",
@@ -136,6 +138,8 @@ export declare enum Constants {
136
138
  WEB_SEARCH = "web_search",
137
139
  CONTENT_AND_ARTIFACT = "content_and_artifact",
138
140
  LC_TRANSFER_TO_ = "lc_transfer_to_",
141
+ /** Prefix for delegate tool names: lc_delegate_to_{agentId} */
142
+ LC_DELEGATE_TO_ = "lc_delegate_to_",
139
143
  /** Tool name for the AskUser structured question tool */
140
144
  ASK_USER = "ask_user",
141
145
  /** Delimiter for MCP tools: toolName_mcp_serverName */
@@ -183,3 +187,7 @@ export declare enum MessageTypes {
183
187
  DEVELOPER = "developer",
184
188
  REMOVE = "remove"
185
189
  }
190
+ /** Default max characters for delegate results returned to parent (~8192 tokens at ~4 chars/token) */
191
+ export declare const DEFAULT_DELEGATE_MAX_RESULT_CHARS = 32768;
192
+ /** Default timeout for delegate sub-agent execution in milliseconds (5 minutes) */
193
+ export declare const DELEGATE_TIMEOUT_MS = 300000;
@@ -1,3 +1,4 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
1
2
  import type * as t from '@/types';
2
3
  import { StandardGraph } from './Graph';
3
4
  /**
@@ -19,6 +20,15 @@ export declare class MultiAgentGraph extends StandardGraph {
19
20
  private startingNodes;
20
21
  private directEdges;
21
22
  private handoffEdges;
23
+ private delegateEdges;
24
+ /**
25
+ * Lazily populated registry of compiled subgraphs, keyed by agentId.
26
+ * Delegate tools are created in the constructor but reference subgraphs
27
+ * that are only created in createWorkflow(). This Map bridges that gap —
28
+ * tools capture the Map reference in their closure, and createWorkflow()
29
+ * populates it before any tool invocation occurs.
30
+ */
31
+ private subgraphRegistry;
22
32
  /**
23
33
  * Map of agentId to parallel group info.
24
34
  * Contains groupId (incrementing number reflecting execution order) for agents in parallel groups.
@@ -99,6 +109,48 @@ export declare class MultiAgentGraph extends StandardGraph {
99
109
  * @param destinationId - Raw agent ID (fallback when context unavailable)
100
110
  */
101
111
  private buildDefaultHandoffDescription;
112
+ /**
113
+ * Create delegate tools for agents based on delegate edges.
114
+ * Delegate tools invoke child agent subgraphs inline and return the result
115
+ * as a string to the parent agent's context. Unlike handoff tools (which
116
+ * return Command for fire-and-forget routing), delegate tools execute the
117
+ * child, extract the final text, and return it within the parent's agent loop.
118
+ *
119
+ * This enables the supervisor pattern: parent calls child → gets result → thinks → calls another.
120
+ */
121
+ private createDelegateTools;
122
+ /**
123
+ * Create delegate tools for an edge (handles multiple destinations).
124
+ * Each delegate tool invokes the child agent's compiled subgraph inline,
125
+ * extracts the final AI message text, truncates it, and returns it as
126
+ * a string (which becomes a ToolMessage in the parent's context).
127
+ *
128
+ * @param edge - The graph edge defining the delegation
129
+ * @param sourceAgentId - The ID of the parent/supervisor agent
130
+ */
131
+ private createDelegateToolsForEdge;
132
+ /**
133
+ * Extract the final text result from a child agent's output messages.
134
+ * Walks backwards to find the last AIMessage with text content.
135
+ * Handles both string content and array content (multi-modal messages).
136
+ * @param messages - The child agent's output messages
137
+ * @param agentId - The child agent ID (for fallback message)
138
+ */
139
+ static extractDelegateResult(messages: BaseMessage[], agentId: string): string;
140
+ /**
141
+ * Truncate delegate result using head/tail strategy (60/40 split).
142
+ * Preserves the beginning (key findings) and end (conclusions).
143
+ * Matches the TaskTool.truncateResult pattern from Ranger.
144
+ * @param result - The full result text
145
+ * @param maxChars - Maximum allowed characters
146
+ */
147
+ static truncateDelegateResult(result: string, maxChars: number): string;
148
+ /**
149
+ * Build a meaningful default description for a delegate tool.
150
+ * @param destContext - AgentContext of the destination agent
151
+ * @param destinationId - Raw agent ID (fallback)
152
+ */
153
+ private buildDefaultDelegateDescription;
102
154
  /**
103
155
  * Create a complete agent subgraph (similar to createReactAgent)
104
156
  */
@@ -242,6 +242,12 @@ export type GraphEdge = {
242
242
  * Only applies when prompt is provided for handoff edges.
243
243
  */
244
244
  promptKey?: string;
245
+ /**
246
+ * For delegate edges: Maximum characters for the result returned to the parent.
247
+ * Uses head/tail truncation (60/40 split) to preserve key findings and conclusions.
248
+ * Defaults to Constants.DEFAULT_DELEGATE_MAX_RESULT_CHARS (32768 chars, ~8192 tokens).
249
+ */
250
+ maxResultChars?: number;
245
251
  };
246
252
  export type MultiAgentGraphInput = StandardGraphInput & {
247
253
  edges: GraphEdge[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illuma-ai/agents",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -26,10 +26,10 @@ describe('EdgeType enum', () => {
26
26
  expect(EdgeType.DIRECT).toBe('direct');
27
27
  });
28
28
 
29
- it('only has two members', () => {
29
+ it('has three members: handoff, direct, delegate', () => {
30
30
  const values = Object.values(EdgeType);
31
- expect(values).toHaveLength(2);
32
- expect(values).toEqual(expect.arrayContaining(['handoff', 'direct']));
31
+ expect(values).toHaveLength(3);
32
+ expect(values).toEqual(expect.arrayContaining(['handoff', 'direct', 'delegate']));
33
33
  });
34
34
  });
35
35
 
@@ -100,6 +100,8 @@ export enum EdgeType {
100
100
  HANDOFF = 'handoff',
101
101
  /** Creates direct edges for automatic sequential/parallel transitions */
102
102
  DIRECT = 'direct',
103
+ /** Creates delegate tools that invoke child subgraph inline and return result to parent */
104
+ DELEGATE = 'delegate',
103
105
  }
104
106
 
105
107
  export enum GraphNodeKeys {
@@ -182,6 +184,8 @@ export enum Constants {
182
184
  WEB_SEARCH = 'web_search',
183
185
  CONTENT_AND_ARTIFACT = 'content_and_artifact',
184
186
  LC_TRANSFER_TO_ = 'lc_transfer_to_',
187
+ /** Prefix for delegate tool names: lc_delegate_to_{agentId} */
188
+ LC_DELEGATE_TO_ = 'lc_delegate_to_',
185
189
  /** Tool name for the AskUser structured question tool */
186
190
  ASK_USER = 'ask_user',
187
191
  /** Delimiter for MCP tools: toolName_mcp_serverName */
@@ -233,3 +237,9 @@ export enum MessageTypes {
233
237
  DEVELOPER = 'developer',
234
238
  REMOVE = 'remove',
235
239
  }
240
+
241
+ /** Default max characters for delegate results returned to parent (~8192 tokens at ~4 chars/token) */
242
+ export const DEFAULT_DELEGATE_MAX_RESULT_CHARS = 32768;
243
+
244
+ /** Default timeout for delegate sub-agent execution in milliseconds (5 minutes) */
245
+ export const DELEGATE_TIMEOUT_MS = 300_000;
@@ -22,7 +22,11 @@ import type { ToolRunnableConfig } from '@langchain/core/tools';
22
22
  import type * as t from '@/types';
23
23
  import { summarize, createEmergencySummary } from '@/messages';
24
24
  import { StandardGraph } from './Graph';
25
- import { Constants, EdgeType } from '@/common';
25
+ import {
26
+ Constants,
27
+ EdgeType,
28
+ DEFAULT_DELEGATE_MAX_RESULT_CHARS,
29
+ } from '@/common';
26
30
 
27
31
  /** Pattern to extract instructions from transfer ToolMessage content */
28
32
  const HANDOFF_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
@@ -46,6 +50,15 @@ export class MultiAgentGraph extends StandardGraph {
46
50
  private startingNodes: Set<string> = new Set();
47
51
  private directEdges: t.GraphEdge[] = [];
48
52
  private handoffEdges: t.GraphEdge[] = [];
53
+ private delegateEdges: t.GraphEdge[] = [];
54
+ /**
55
+ * Lazily populated registry of compiled subgraphs, keyed by agentId.
56
+ * Delegate tools are created in the constructor but reference subgraphs
57
+ * that are only created in createWorkflow(). This Map bridges that gap —
58
+ * tools capture the Map reference in their closure, and createWorkflow()
59
+ * populates it before any tool invocation occurs.
60
+ */
61
+ private subgraphRegistry: Map<string, t.CompiledAgentWorfklow> = new Map();
49
62
  /**
50
63
  * Map of agentId to parallel group info.
51
64
  * Contains groupId (incrementing number reflecting execution order) for agents in parallel groups.
@@ -69,6 +82,7 @@ export class MultiAgentGraph extends StandardGraph {
69
82
  this.categorizeEdges();
70
83
  this.analyzeGraph();
71
84
  this.createHandoffTools();
85
+ this.createDelegateTools();
72
86
  console.debug(
73
87
  `[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`
74
88
  );
@@ -81,7 +95,9 @@ export class MultiAgentGraph extends StandardGraph {
81
95
  for (const edge of this.edges) {
82
96
  // Default behavior: edges with conditions or explicit 'handoff' type are handoff edges
83
97
  // Edges with explicit 'direct' type or multi-destination without conditions are direct edges
84
- if (edge.edgeType === EdgeType.DIRECT) {
98
+ if (edge.edgeType === EdgeType.DELEGATE) {
99
+ this.delegateEdges.push(edge);
100
+ } else if (edge.edgeType === EdgeType.DIRECT) {
85
101
  this.directEdges.push(edge);
86
102
  } else if (edge.edgeType === EdgeType.HANDOFF || edge.condition != null) {
87
103
  this.handoffEdges.push(edge);
@@ -100,7 +116,7 @@ export class MultiAgentGraph extends StandardGraph {
100
116
  }
101
117
  }
102
118
  console.debug(
103
- `[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.directEdges.length} direct (of ${this.edges.length} total)`
119
+ `[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.directEdges.length} direct, ${this.delegateEdges.length} delegate (of ${this.edges.length} total)`
104
120
  );
105
121
  }
106
122
 
@@ -200,8 +216,8 @@ export class MultiAgentGraph extends StandardGraph {
200
216
  }
201
217
  }
202
218
 
203
- // Also follow handoff edges for traversal (but they don't create parallel groups)
204
- for (const edge of this.handoffEdges) {
219
+ // Also follow handoff and delegate edges for traversal (they don't create parallel groups)
220
+ for (const edge of [...this.handoffEdges, ...this.delegateEdges]) {
205
221
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
206
222
  if (!sources.includes(current)) continue;
207
223
 
@@ -547,6 +563,269 @@ export class MultiAgentGraph extends StandardGraph {
547
563
  return `Transfer control to "${displayName}"`;
548
564
  }
549
565
 
566
+ /**
567
+ * Create delegate tools for agents based on delegate edges.
568
+ * Delegate tools invoke child agent subgraphs inline and return the result
569
+ * as a string to the parent agent's context. Unlike handoff tools (which
570
+ * return Command for fire-and-forget routing), delegate tools execute the
571
+ * child, extract the final text, and return it within the parent's agent loop.
572
+ *
573
+ * This enables the supervisor pattern: parent calls child → gets result → thinks → calls another.
574
+ */
575
+ private createDelegateTools(): void {
576
+ const delegatesByAgent = new Map<string, t.GraphEdge[]>();
577
+
578
+ for (const edge of this.delegateEdges) {
579
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
580
+ sources.forEach((source) => {
581
+ if (!delegatesByAgent.has(source)) {
582
+ delegatesByAgent.set(source, []);
583
+ }
584
+ delegatesByAgent.get(source)!.push(edge);
585
+ });
586
+ }
587
+
588
+ for (const [agentId, edges] of delegatesByAgent) {
589
+ const agentContext = this.agentContexts.get(agentId);
590
+ if (!agentContext) continue;
591
+
592
+ const delegateTools: t.GenericTool[] = [];
593
+ for (const edge of edges) {
594
+ delegateTools.push(
595
+ ...this.createDelegateToolsForEdge(edge, agentId)
596
+ );
597
+ }
598
+
599
+ if (!agentContext.graphTools) {
600
+ agentContext.graphTools = [];
601
+ }
602
+ agentContext.graphTools.push(...delegateTools);
603
+ console.debug(
604
+ `[MultiAgentGraph] Delegate tools for "${agentId}": [${delegateTools.map((t) => t.name).join(', ')}]`
605
+ );
606
+ }
607
+ }
608
+
609
+ /**
610
+ * Create delegate tools for an edge (handles multiple destinations).
611
+ * Each delegate tool invokes the child agent's compiled subgraph inline,
612
+ * extracts the final AI message text, truncates it, and returns it as
613
+ * a string (which becomes a ToolMessage in the parent's context).
614
+ *
615
+ * @param edge - The graph edge defining the delegation
616
+ * @param sourceAgentId - The ID of the parent/supervisor agent
617
+ */
618
+ private createDelegateToolsForEdge(
619
+ edge: t.GraphEdge,
620
+ sourceAgentId: string
621
+ ): t.GenericTool[] {
622
+ const tools: t.GenericTool[] = [];
623
+ const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
624
+ const maxResultChars =
625
+ edge.maxResultChars ?? DEFAULT_DELEGATE_MAX_RESULT_CHARS;
626
+
627
+ for (const destination of destinations) {
628
+ const toolName = `${Constants.LC_DELEGATE_TO_}${destination}`;
629
+ const destContext = this.agentContexts.get(destination);
630
+ const toolDescription =
631
+ edge.description ??
632
+ this.buildDefaultDelegateDescription(destContext, destination);
633
+
634
+ const hasPromptInput =
635
+ edge.prompt != null && typeof edge.prompt === 'string';
636
+ const promptInputDescription = hasPromptInput ? edge.prompt : undefined;
637
+ const promptKey = edge.promptKey ?? 'instructions';
638
+
639
+ /** Capture registry reference — Map populated in createWorkflow() */
640
+ const registry = this.subgraphRegistry;
641
+
642
+ tools.push(
643
+ tool(
644
+ async (rawInput, config) => {
645
+ const input = rawInput as Record<string, unknown>;
646
+ const subgraph = registry.get(destination);
647
+ if (!subgraph) {
648
+ throw new Error(
649
+ `Delegate target "${destination}" subgraph not found in registry. ` +
650
+ 'This is a bug: createWorkflow() should have populated the subgraph registry.'
651
+ );
652
+ }
653
+
654
+ const state = getCurrentTaskInput() as t.BaseGraphState;
655
+ let childMessages = [...state.messages];
656
+
657
+ /** Inject instructions as HumanMessage if provided by the parent LLM */
658
+ if (
659
+ hasPromptInput &&
660
+ promptKey in input &&
661
+ input[promptKey] != null
662
+ ) {
663
+ childMessages = [
664
+ ...childMessages,
665
+ new HumanMessage(String(input[promptKey])),
666
+ ];
667
+ }
668
+
669
+ const childState: t.BaseGraphState = {
670
+ messages: childMessages,
671
+ };
672
+
673
+ console.debug(
674
+ `[MultiAgentGraph] Delegate "${sourceAgentId}" -> "${destination}" START ` +
675
+ `(messages: ${childMessages.length})`
676
+ );
677
+
678
+ try {
679
+ /**
680
+ * Invoke the child subgraph with config propagation.
681
+ * Config carries callbacks (for SSE streaming), abort signal,
682
+ * and configurable data (thread_id, user_id) to the child.
683
+ */
684
+ const result = await subgraph.invoke(childState, config);
685
+
686
+ const resultText = MultiAgentGraph.extractDelegateResult(
687
+ result.messages,
688
+ destination
689
+ );
690
+ const truncatedResult = MultiAgentGraph.truncateDelegateResult(
691
+ resultText,
692
+ maxResultChars
693
+ );
694
+
695
+ console.debug(
696
+ `[MultiAgentGraph] Delegate "${sourceAgentId}" -> "${destination}" DONE ` +
697
+ `(result: ${resultText.length} chars` +
698
+ `${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`
699
+ );
700
+
701
+ return truncatedResult;
702
+ } catch (err) {
703
+ const errorMessage =
704
+ err instanceof Error ? err.message : String(err);
705
+ console.error(
706
+ `[MultiAgentGraph] Delegate "${sourceAgentId}" -> "${destination}" ERROR:`,
707
+ errorMessage
708
+ );
709
+ return `[Delegate to "${destination}" failed: ${errorMessage}]`;
710
+ }
711
+ },
712
+ {
713
+ name: toolName,
714
+ schema: hasPromptInput
715
+ ? {
716
+ type: 'object',
717
+ properties: {
718
+ [promptKey]: {
719
+ type: 'string',
720
+ description: promptInputDescription as string,
721
+ },
722
+ },
723
+ required: [],
724
+ }
725
+ : { type: 'object', properties: {}, required: [] },
726
+ description: toolDescription,
727
+ }
728
+ )
729
+ );
730
+ }
731
+
732
+ return tools;
733
+ }
734
+
735
+ /**
736
+ * Extract the final text result from a child agent's output messages.
737
+ * Walks backwards to find the last AIMessage with text content.
738
+ * Handles both string content and array content (multi-modal messages).
739
+ * @param messages - The child agent's output messages
740
+ * @param agentId - The child agent ID (for fallback message)
741
+ */
742
+ static extractDelegateResult(
743
+ messages: BaseMessage[],
744
+ agentId: string
745
+ ): string {
746
+ for (let i = messages.length - 1; i >= 0; i--) {
747
+ const msg = messages[i];
748
+ if (msg.getType() !== 'ai') continue;
749
+
750
+ const content = msg.content;
751
+ if (typeof content === 'string' && content.trim()) {
752
+ return content.trim();
753
+ }
754
+
755
+ /** Handle array content (multi-modal messages with text blocks) */
756
+ if (Array.isArray(content)) {
757
+ const textParts = content
758
+ .filter(
759
+ (
760
+ block
761
+ ): block is {
762
+ type: string;
763
+ text: string;
764
+ } =>
765
+ typeof block === 'object' &&
766
+ block !== null &&
767
+ 'type' in block &&
768
+ block.type === 'text' &&
769
+ 'text' in block &&
770
+ typeof block.text === 'string'
771
+ )
772
+ .map((block) => block.text);
773
+
774
+ const text = textParts.join('\n').trim();
775
+ if (text) return text;
776
+ }
777
+ }
778
+
779
+ return `[Agent "${agentId}" completed but produced no text output]`;
780
+ }
781
+
782
+ /**
783
+ * Truncate delegate result using head/tail strategy (60/40 split).
784
+ * Preserves the beginning (key findings) and end (conclusions).
785
+ * Matches the TaskTool.truncateResult pattern from Ranger.
786
+ * @param result - The full result text
787
+ * @param maxChars - Maximum allowed characters
788
+ */
789
+ static truncateDelegateResult(result: string, maxChars: number): string {
790
+ if (!result || result.length <= maxChars) {
791
+ return result;
792
+ }
793
+
794
+ const truncationNotice =
795
+ '\n\n[... delegate output truncated — middle section omitted to fit parent context ...]\n\n';
796
+ const available = maxChars - truncationNotice.length;
797
+ if (available <= 0) {
798
+ return result.substring(0, maxChars);
799
+ }
800
+
801
+ const headSize = Math.floor(available * 0.6);
802
+ const tailSize = available - headSize;
803
+
804
+ return (
805
+ result.substring(0, headSize) +
806
+ truncationNotice +
807
+ result.substring(result.length - tailSize)
808
+ );
809
+ }
810
+
811
+ /**
812
+ * Build a meaningful default description for a delegate tool.
813
+ * @param destContext - AgentContext of the destination agent
814
+ * @param destinationId - Raw agent ID (fallback)
815
+ */
816
+ private buildDefaultDelegateDescription(
817
+ destContext: import('@/agents/AgentContext').AgentContext | undefined,
818
+ destinationId: string
819
+ ): string {
820
+ const displayName = destContext?.name ?? destinationId;
821
+ const agentDescription = destContext?.description;
822
+
823
+ if (agentDescription != null && agentDescription !== '') {
824
+ return `Delegate task to "${displayName}": ${agentDescription}. The agent will execute and return its result.`;
825
+ }
826
+ return `Delegate task to "${displayName}" and receive its result.`;
827
+ }
828
+
550
829
  /**
551
830
  * Create a complete agent subgraph (similar to createReactAgent)
552
831
  */
@@ -925,6 +1204,9 @@ export class MultiAgentGraph extends StandardGraph {
925
1204
  /** Agent subgraph (includes agent + tools) */
926
1205
  const agentSubgraph = this.createAgentSubgraph(agentId);
927
1206
 
1207
+ /** Register subgraph for delegate tools (lazy reference resolution) */
1208
+ this.subgraphRegistry.set(agentId, agentSubgraph);
1209
+
928
1210
  /** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
929
1211
  const agentWrapper = async (
930
1212
  state: t.MultiAgentGraphState,