66import dev .langchain4j .agentic .AgenticServices ;
77import dev .langchain4j .agentic .agent .AgentBuilder ;
88import dev .langchain4j .agentic .declarative .PlannerSupplier ;
9+ import dev .langchain4j .agentic .declarative .TypedKey ;
910import dev .langchain4j .agentic .internal .AgentExecutor ;
1011import dev .langchain4j .agentic .internal .AgentInvoker ;
1112import dev .langchain4j .agentic .internal .AgentUtil ;
@@ -156,9 +157,14 @@ private static <X> X createForSimple(Instance<Object> lookup, Class<X> interface
156157 if (hasText (resolvedDescription )) {
157158 builder .description (resolvedDescription );
158159 }
159- String resolvedOutputKey = CdiLookupHelper .resolveExpression (ann .outputKey ());
160- if (hasText (resolvedOutputKey )) {
161- builder .outputKey (resolvedOutputKey );
160+ if (ann .typedOutputKey () != Agent .NoTypedKey .class ) {
161+ warnIfBothOutputKeysSet (ann .outputKey (), ann .typedOutputKey ());
162+ builder .outputKey (ann .typedOutputKey ());
163+ } else {
164+ String resolvedOutputKey = CdiLookupHelper .resolveExpression (ann .outputKey ());
165+ if (hasText (resolvedOutputKey )) {
166+ builder .outputKey (resolvedOutputKey );
167+ }
162168 }
163169 builder .async (ann .async ());
164170 builder .optional (ann .optional ());
@@ -174,7 +180,7 @@ private static <X> X createForSimple(Instance<Object> lookup, Class<X> interface
174180 X aiService = buildAiServiceForSimple (interfaceClass , ann , chatModel , streamingChatModel , lookup );
175181 String description = CdiLookupHelper .resolveExpression (ann .description ());
176182 if (!hasText (description )) description = "" ;
177- String outputKey = CdiLookupHelper . resolveExpression ( ann .outputKey ());
183+ String outputKey = resolveOutputKey ( ann . typedOutputKey (), ann .outputKey ());
178184 if (!hasText (outputKey )) outputKey = "" ;
179185 AgentListener listener = CdiLookupHelper .resolveSingle (lookup , AgentListener .class , ann .agentListenerName ());
180186 NonAiAgentInstance agentInstance = buildNonAiAgentInstance (
@@ -231,6 +237,7 @@ private static <X> X createForSequence(
231237 ann .name (),
232238 ann .description (),
233239 ann .outputKey (),
240+ ann .typedOutputKey (),
234241 ann .subAgentNames (),
235242 ann .agentListenerName (),
236243 lookup );
@@ -250,6 +257,7 @@ private static <X> X createForLoop(Instance<Object> lookup, Class<X> interfaceCl
250257 ann .name (),
251258 ann .description (),
252259 ann .outputKey (),
260+ ann .typedOutputKey (),
253261 ann .subAgentNames (),
254262 ann .agentListenerName (),
255263 lookup );
@@ -262,6 +270,7 @@ private static <X> X createForParallel(
262270 ann .name (),
263271 ann .description (),
264272 ann .outputKey (),
273+ ann .typedOutputKey (),
265274 ann .subAgentNames (),
266275 ann .agentListenerName (),
267276 lookup );
@@ -281,6 +290,7 @@ private static <X> X createForParallelMapper(
281290 ann .name (),
282291 ann .description (),
283292 ann .outputKey (),
293+ ann .typedOutputKey (),
284294 ann .subAgentNames (),
285295 ann .agentListenerName (),
286296 lookup );
@@ -293,6 +303,7 @@ private static <X> X createForConditional(
293303 ann .name (),
294304 ann .description (),
295305 ann .outputKey (),
306+ ann .typedOutputKey (),
296307 ann .subAgentNames (),
297308 ann .agentListenerName (),
298309 lookup );
@@ -318,14 +329,17 @@ private static <X> X createForSupervisor(
318329 builder .supervisorContext (supervisorContext );
319330 }
320331 List <Object > subAgents = resolveSubAgents (lookup , ann .subAgentNames ());
332+ String resolvedOutputKey = resolveOutputKey (ann .typedOutputKey (), ann .outputKey ());
321333 configureCommonFields (
322334 builder ::subAgents ,
323335 builder ::name ,
324336 builder ::description ,
325337 builder ::outputKey ,
338+ null ,
326339 ann .name (),
327340 ann .description (),
328- ann .outputKey (),
341+ resolvedOutputKey ,
342+ Agent .NoTypedKey .class ,
329343 subAgents );
330344 applyListener (builder ::listener , ann .agentListenerName (), lookup );
331345 return builder .build ();
@@ -344,6 +358,7 @@ private static <X> X createForPlanner(Instance<Object> lookup, Class<X> interfac
344358 ann .name (),
345359 ann .description (),
346360 ann .outputKey (),
361+ ann .typedOutputKey (),
347362 ann .subAgentNames (),
348363 ann .agentListenerName (),
349364 lookup );
@@ -355,15 +370,9 @@ private static <X> X createForA2A(Instance<Object> lookup, Class<X> interfaceCla
355370 throw new IllegalArgumentException (
356371 "@RegisterA2AAgent on " + interfaceClass .getSimpleName () + ": 'a2aServerUrl' is required." );
357372 }
373+ String outputKey = resolveOutputKey (ann .typedOutputKey (), ann .outputKey ());
358374 X a2aAgent = loadA2ABuilder ()
359- .build (
360- interfaceClass ,
361- url ,
362- CdiLookupHelper .resolveExpression (ann .outputKey ()),
363- ann .async (),
364- ann .optional (),
365- ann .agentListenerName (),
366- lookup );
375+ .build (interfaceClass , url , outputKey , ann .async (), ann .optional (), ann .agentListenerName (), lookup );
367376 return wrapWithAgenticScope (interfaceClass , a2aAgent );
368377 }
369378
@@ -390,7 +399,7 @@ private static <X> X createForMcpClient(
390399 if (ann .mcpInputKeys ().length > 0 ) {
391400 builder .inputKeys (ann .mcpInputKeys ());
392401 }
393- String outputKey = CdiLookupHelper . resolveExpression ( ann .outputKey ());
402+ String outputKey = resolveOutputKey ( ann . typedOutputKey (), ann .outputKey ());
394403 if (hasText (outputKey )) {
395404 builder .outputKey (outputKey );
396405 }
@@ -419,7 +428,7 @@ private static <X> X createForHumanInTheLoop(
419428 throw new RuntimeException (cause );
420429 }
421430 });
422- String outputKey = CdiLookupHelper . resolveExpression ( ann .outputKey ());
431+ String outputKey = resolveOutputKey ( ann . typedOutputKey (), ann .outputKey ());
423432 if (hasText (outputKey )) {
424433 builder .outputKey (outputKey );
425434 }
@@ -558,6 +567,7 @@ private static <X, S extends AgenticService<S, X>> X buildComposed(
558567 String name ,
559568 String description ,
560569 String outputKey ,
570+ Class <? extends TypedKey <?>> typedOutputKey ,
561571 String [] subAgentNames ,
562572 String agentListenerName ,
563573 Instance <Object > lookup ) {
@@ -567,9 +577,11 @@ private static <X, S extends AgenticService<S, X>> X buildComposed(
567577 builder ::name ,
568578 builder ::description ,
569579 builder ::outputKey ,
580+ typedOutputKey != Agent .NoTypedKey .class ? builder ::outputKey : null ,
570581 name ,
571582 description ,
572583 outputKey ,
584+ typedOutputKey ,
573585 subAgents );
574586 applyListener (builder ::listener , agentListenerName , lookup );
575587 return builder .build ();
@@ -580,9 +592,11 @@ private static void configureCommonFields(
580592 Consumer <String > nameSetter ,
581593 Consumer <String > descriptionSetter ,
582594 Consumer <String > outputKeySetter ,
595+ Consumer <Class <? extends TypedKey <?>>> typedOutputKeySetter ,
583596 String rawName ,
584597 String rawDescription ,
585598 String rawOutputKey ,
599+ Class <? extends TypedKey <?>> typedOutputKey ,
586600 List <Object > subAgents ) {
587601 if (!subAgents .isEmpty ()) {
588602 subAgentsSetter .accept (subAgents );
@@ -595,9 +609,53 @@ private static void configureCommonFields(
595609 if (hasText (description )) {
596610 descriptionSetter .accept (description );
597611 }
598- String outputKey = CdiLookupHelper .resolveExpression (rawOutputKey );
599- if (hasText (outputKey )) {
600- outputKeySetter .accept (outputKey );
612+ if (typedOutputKey != null && typedOutputKey != Agent .NoTypedKey .class ) {
613+ warnIfBothOutputKeysSet (rawOutputKey , typedOutputKey );
614+ if (typedOutputKeySetter != null ) {
615+ typedOutputKeySetter .accept (typedOutputKey );
616+ } else {
617+ String resolved = resolveTypedKeyName (typedOutputKey );
618+ if (hasText (resolved )) {
619+ outputKeySetter .accept (resolved );
620+ }
621+ }
622+ } else {
623+ String outputKey = CdiLookupHelper .resolveExpression (rawOutputKey );
624+ if (hasText (outputKey )) {
625+ outputKeySetter .accept (outputKey );
626+ }
627+ }
628+ }
629+
630+ static String resolveOutputKey (Class <? extends TypedKey <?>> typedOutputKey , String rawOutputKey ) {
631+ if (typedOutputKey != null && typedOutputKey != Agent .NoTypedKey .class ) {
632+ warnIfBothOutputKeysSet (rawOutputKey , typedOutputKey );
633+ return resolveTypedKeyName (typedOutputKey );
634+ }
635+ return CdiLookupHelper .resolveExpression (rawOutputKey );
636+ }
637+
638+ private static void warnIfBothOutputKeysSet (String rawOutputKey , Class <? extends TypedKey <?>> typedOutputKey ) {
639+ if (hasText (rawOutputKey )) {
640+ LOGGER .log (
641+ Level .WARNING ,
642+ "Both outputKey (''{0}'') and typedOutputKey ({1}) are set; typedOutputKey takes precedence" ,
643+ new Object [] {rawOutputKey , typedOutputKey .getName ()});
644+ }
645+ }
646+
647+ static String resolveTypedKeyName (Class <? extends TypedKey <?>> typedKeyClass ) {
648+ try {
649+ TypedKey <?> instance = typedKeyClass .getDeclaredConstructor ().newInstance ();
650+ String name = instance .name ();
651+ return hasText (name ) ? name : typedKeyClass .getSimpleName ();
652+ } catch (Exception e ) {
653+ LOGGER .log (
654+ Level .WARNING ,
655+ e ,
656+ () -> "Cannot instantiate TypedKey class " + typedKeyClass .getName ()
657+ + " (no accessible no-arg constructor?), falling back to simple class name" );
658+ return typedKeyClass .getSimpleName ();
601659 }
602660 }
603661
@@ -939,7 +997,7 @@ private static AgentExecutor tryBuildExecutor(Object bean, InternalAgent ia, Cla
939997 String resolvedName = meta .name ();
940998 String name = hasText (resolvedName ) ? resolvedName : entryMethod .getName ();
941999 String desc = meta .description ();
942- String outputKey = meta .outputKey ( );
1000+ String outputKey = resolveOutputKey ( meta .rawTypedOutputKey (), meta . rawOutputKey () );
9431001 boolean async = meta .async ();
9441002 boolean optional = meta .optional ();
9451003 AgentInvoker invoker ;
0 commit comments