@@ -678,16 +678,8 @@ protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) {
678678 () -> {
679679 // Resumed after a transfer: continue the sub-agent instead of re-invoking the model.
680680 if (invocationContext .agentStates ().containsKey (name ())) {
681- Optional <String > resumeTargetName = findTransferToResume (invocationContext );
682- if (resumeTargetName .isPresent ()) {
683- Optional <BaseAgent > resumeTarget = declaredTransferTarget (resumeTargetName .get ());
684- // End this agent without running anything if the recorded target is undeclared
685- // (rejected) or already ended after this agent's latest transfer (e.g. on the
686- // target's own rejected transfer).
687- if (resumeTarget .isEmpty ()
688- || endedAfterLatestTransfer (invocationContext , resumeTarget .get ())) {
689- return endOfAgentAndRecord (invocationContext );
690- }
681+ Optional <BaseAgent > resumeTarget = findSubAgentToResume (invocationContext );
682+ if (resumeTarget .isPresent ()) {
691683 AtomicBoolean resumePaused = new AtomicBoolean (false );
692684 return resumeTarget
693685 .get ()
@@ -719,7 +711,6 @@ protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) {
719711 paused .set (true );
720712 }
721713 })
722- .onErrorResumeNext (error -> endIfOwnRejectedTransfer (invocationContext , error ))
723714 .concatWith (
724715 Flowable .defer (
725716 () -> {
@@ -734,20 +725,6 @@ protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) {
734725 });
735726 }
736727
737- /**
738- * Emits an end-of-agent marker before {@code error} when it is a {@link
739- * TransferRejectedException} from this agent, so a resume does not re-run it. Any other error,
740- * including a sub-agent's rejection, passes through and leaves this agent open.
741- */
742- private Flowable <Event > endIfOwnRejectedTransfer (
743- InvocationContext invocationContext , Throwable error ) {
744- if (error instanceof TransferRejectedException rejected
745- && rejected .agentName ().equals (name ())) {
746- return endOfAgentAndRecord (invocationContext ).concatWith (Flowable .error (error ));
747- }
748- return Flowable .error (error );
749- }
750-
751728 /**
752729 * Returns the name of the agent this agent transferred to in {@code event}, or empty when {@code
753730 * event} is not such a transfer. A {@code transfer_to_agent} function response is required, as in
@@ -764,56 +741,41 @@ private Optional<String> transferTargetNameFrom(Event event) {
764741 }
765742
766743 /**
767- * Returns whether {@code target} ended after this agent's latest transfer, as it does on its own
768- * rejected transfer. An end marker from before that transfer, such as a past loop iteration, does
769- * not count .
744+ * Resolves a transfer target recorded in history back to its agent. A live transfer fails in
745+ * {@code BaseLlmFlow} when the target is gone; a resumed one has no such check, and returning
746+ * empty here would silently re-run this agent instead of the sub-agent it handed off to .
770747 */
771- private boolean endedAfterLatestTransfer (InvocationContext context , BaseAgent target ) {
772- // All branches: under a ParallelAgent, the target writes its marker on a descendant branch.
773- for (Event event :
774- context .events (/* currentInvocation= */ true , /* currentBranch= */ false ).reverse ()) {
775- if (transferTargetNameFrom (event ).isPresent ()) {
776- return false ;
777- }
778- if (isEndMarker (event , target .name ())) {
779- return true ;
748+ private BaseAgent resolveTransferTarget (String targetName ) {
749+ // Declared targets first, as in Python, so a same-named agent on an unrelated branch is not
750+ // resumed into; then the same tree-wide search the live transfer path uses.
751+ for (BaseAgent target : transferTargets ()) {
752+ if (target .name ().equals (targetName )) {
753+ return target ;
780754 }
781755 }
782- return false ;
756+ return rootAgent ()
757+ .findAgent (targetName )
758+ .orElseThrow (
759+ () ->
760+ new IllegalStateException (
761+ "Cannot resume agent "
762+ + name ()
763+ + ": the agent it transferred to is no longer in the agent tree." ));
783764 }
784765
785766 /**
786- * Returns whether {@code event} is {@code author}'s end-of-agent marker, which has no content.
767+ * When this agent is being resumed, returns the sub-agent it had transferred to (so the resume
768+ * continues that sub-agent), or empty when this agent should continue itself.
787769 */
788- private static boolean isEndMarker (Event event , String author ) {
789- return author .equals (event .author ())
790- && event .actions ().endOfAgent ()
791- && event .content ().isEmpty ();
792- }
793-
794- /**
795- * Returns the declared target named {@code targetName}, or empty when it is not one, so a
796- * same-named agent elsewhere in the tree is never resumed in its place.
797- */
798- private Optional <BaseAgent > declaredTransferTarget (String targetName ) {
799- return transferTargets ().stream ()
800- .filter (target -> target .name ().equals (targetName ))
801- .findFirst ();
802- }
803-
804- /**
805- * When this agent is being resumed, returns the name of the agent it had transferred to, or empty
806- * when this agent should continue itself. The name need not be a declared target.
807- */
808- private Optional <String > findTransferToResume (InvocationContext context ) {
770+ private Optional <BaseAgent > findSubAgentToResume (InvocationContext context ) {
809771 ImmutableList <Event > events =
810772 context .events (/* currentInvocation= */ true , /* currentBranch= */ true );
811773 if (events .isEmpty ()) {
812774 return Optional .empty ();
813775 }
814776 Event lastEvent = Iterables .getLast (events );
815777 if (name ().equals (lastEvent .author ())) {
816- return transferTargetNameFrom (lastEvent );
778+ return transferTargetNameFrom (lastEvent ). map ( this :: resolveTransferTarget ) ;
817779 }
818780 if (Objects .equals (lastEvent .author (), Role .USER ) && !lastEvent .functionResponses ().isEmpty ()) {
819781 // IAE (not ISE): an unresolvable resume surfaces through Runner.runAsync's IAE contract.
@@ -832,14 +794,9 @@ private Optional<String> findTransferToResume(InvocationContext context) {
832794 }
833795 // A user event answering no call falls through; Python fails the resume instead.
834796 for (int i = events .size () - 2 ; i >= 0 ; i --) {
835- Event event = events .get (i );
836- // Transfers before this agent's own end marker belong to a finished run, such as a past loop.
837- if (isEndMarker (event , name ())) {
838- break ;
839- }
840- Optional <String > targetName = transferTargetNameFrom (event );
797+ Optional <String > targetName = transferTargetNameFrom (events .get (i ));
841798 if (targetName .isPresent ()) {
842- return targetName ;
799+ return Optional . of ( resolveTransferTarget ( targetName . get ())) ;
843800 }
844801 }
845802 return Optional .empty ();
0 commit comments