|
60 | 60 | import io.reactivex.rxjava3.core.Flowable; |
61 | 61 | import io.reactivex.rxjava3.core.Single; |
62 | 62 | import io.reactivex.rxjava3.subscribers.TestSubscriber; |
| 63 | +import java.util.ArrayList; |
63 | 64 | import java.util.List; |
64 | 65 | import java.util.Map; |
65 | 66 | import java.util.Optional; |
@@ -242,6 +243,138 @@ public void runLive_transferToAgent_closesConnection() throws Exception { |
242 | 243 | assertThat(closedConnectionsCount).isEqualTo(1); |
243 | 244 | } |
244 | 245 |
|
| 246 | + @Test |
| 247 | + public void runLive_transferToGrandchild_failsAndGrandchildNeverRuns() { |
| 248 | + TestLlm grandchildLlm = TestLlm.create(createTextLlmResponse("grandchild ran")); |
| 249 | + LlmAgent root = rootTransferringToGrandchild(grandchildLlm); |
| 250 | + |
| 251 | + TransferRejectedException error = |
| 252 | + assertThrows(TransferRejectedException.class, () -> runLive(root)); |
| 253 | + |
| 254 | + assertThat(error) |
| 255 | + .hasMessageThat() |
| 256 | + .isEqualTo("Agent 'root' is not allowed to transfer to the requested agent."); |
| 257 | + assertThat(grandchildLlm.getRequests()).isEmpty(); |
| 258 | + } |
| 259 | + |
| 260 | + @Test |
| 261 | + public void runLive_transferToParent_withDisallowTransferToParent_fails() { |
| 262 | + // A second connection, so a wrongly re-run root ends the run instead of transferring again. |
| 263 | + TestLlm rootLlm = |
| 264 | + createTestLlm( |
| 265 | + Flowable.just(transferTo("sub")), |
| 266 | + Flowable.just(createTextLlmResponse("root ran again"))); |
| 267 | + LlmAgent root = rootWithSubDisallowingParent(rootLlm); |
| 268 | + |
| 269 | + TransferRejectedException error = |
| 270 | + assertThrows(TransferRejectedException.class, () -> runLive(root)); |
| 271 | + |
| 272 | + assertThat(error) |
| 273 | + .hasMessageThat() |
| 274 | + .isEqualTo("Agent 'sub' is not allowed to transfer to the requested agent."); |
| 275 | + assertThat(rootLlm.getRequests()).hasSize(1); |
| 276 | + } |
| 277 | + |
| 278 | + @Test |
| 279 | + public void runLive_transferToPeer_withDisallowTransferToPeers_failsAndPeerNeverRuns() { |
| 280 | + TestLlm peerLlm = TestLlm.create(ImmutableList.of()); |
| 281 | + LlmAgent root = rootWithSubDisallowingPeers(TestLlm.create(transferTo("peer")), peerLlm); |
| 282 | + |
| 283 | + TransferRejectedException error = |
| 284 | + assertThrows(TransferRejectedException.class, () -> runLive(root)); |
| 285 | + |
| 286 | + assertThat(error) |
| 287 | + .hasMessageThat() |
| 288 | + .isEqualTo("Agent 'sub' is not allowed to transfer to the requested agent."); |
| 289 | + assertThat(error.agentName()).isEqualTo("sub"); |
| 290 | + assertThat(peerLlm.getRequests()).isEmpty(); |
| 291 | + } |
| 292 | + |
| 293 | + // The model chose the name, so the error names only the calling agent. |
| 294 | + @Test |
| 295 | + public void runLive_transferToUnknownAgent_failsAsNotInTreeAfterClosingTheConnection() { |
| 296 | + TestLlm rootLlm = TestLlm.create(transferTo("missing")); |
| 297 | + LlmAgent root = root(rootLlm, textAgent("sub")); |
| 298 | + List<Event> events = new ArrayList<>(); |
| 299 | + |
| 300 | + TransferRejectedException error = |
| 301 | + assertThrows(TransferRejectedException.class, () -> runLive(root, events)); |
| 302 | + |
| 303 | + assertThat(error) |
| 304 | + .hasMessageThat() |
| 305 | + .isEqualTo("Agent 'root' requested a transfer to an agent that is not in the agent tree."); |
| 306 | + assertThat(simplifyEvents(events)) |
| 307 | + .containsExactly( |
| 308 | + "root: FunctionCall(name=transfer_to_agent, args={agent_name=missing})", |
| 309 | + "root: FunctionResponse(name=transfer_to_agent, response={})") |
| 310 | + .inOrder(); |
| 311 | + assertThat(rootLlm.getLiveRequestHistory().stream().filter(LiveRequest::shouldClose).count()) |
| 312 | + .isEqualTo(1); |
| 313 | + } |
| 314 | + |
| 315 | + @Test |
| 316 | + public void runLive_transferToDuplicateName_runsTheDeclaredTarget() { |
| 317 | + TestLlm declaredLlm = TestLlm.create(createTextLlmResponse("declared ran")); |
| 318 | + TestLlm namesakeLlm = TestLlm.create(createTextLlmResponse("namesake ran")); |
| 319 | + |
| 320 | + ImmutableList<Event> events = runLive(rootWithNamesakeTargets(declaredLlm, namesakeLlm)); |
| 321 | + |
| 322 | + assertThat(simplifyEvents(events)).contains("shared: declared ran"); |
| 323 | + assertThat(declaredLlm.getRequests()).hasSize(1); |
| 324 | + assertThat(namesakeLlm.getRequests()).isEmpty(); |
| 325 | + } |
| 326 | + |
| 327 | + // Peers and the parent are declared targets too, not only sub-agents. |
| 328 | + @Test |
| 329 | + public void runLive_transferToPeerThenParent_runsBoth() { |
| 330 | + TestLlm rootLlm = |
| 331 | + createTestLlm( |
| 332 | + Flowable.just(transferTo("sub")), Flowable.just(createTextLlmResponse("root again"))); |
| 333 | + TestLlm peerLlm = TestLlm.create(transferTo("root")); |
| 334 | + LlmAgent sub = createTestAgentBuilder(TestLlm.create(transferTo("peer"))).name("sub").build(); |
| 335 | + LlmAgent peer = createTestAgentBuilder(peerLlm).name("peer").build(); |
| 336 | + |
| 337 | + ImmutableList<Event> events = runLive(root(rootLlm, sub, peer)); |
| 338 | + |
| 339 | + assertThat(simplifyEvents(events)).contains("root: root again"); |
| 340 | + assertThat(peerLlm.getRequests()).hasSize(1); |
| 341 | + assertThat(rootLlm.getRequests()).hasSize(2); |
| 342 | + } |
| 343 | + |
| 344 | + // runAsync rejects a transfer to self; a live run reopens the agent on a new connection. |
| 345 | + @Test |
| 346 | + public void runLive_transferToSelf_reopensTheAgent() { |
| 347 | + TestLlm rootLlm = |
| 348 | + createTestLlm( |
| 349 | + Flowable.just(transferTo("root")), |
| 350 | + Flowable.just(createTextLlmResponse("root reopened"))); |
| 351 | + |
| 352 | + ImmutableList<Event> events = runLive(root(rootLlm, textAgent("sub"))); |
| 353 | + |
| 354 | + assertThat(simplifyEvents(events)) |
| 355 | + .containsExactly( |
| 356 | + "root: FunctionCall(name=transfer_to_agent, args={agent_name=root})", |
| 357 | + "root: FunctionResponse(name=transfer_to_agent, response={})", |
| 358 | + "root: root reopened") |
| 359 | + .inOrder(); |
| 360 | + assertThat(rootLlm.getRequests()).hasSize(2); |
| 361 | + } |
| 362 | + |
| 363 | + @Test |
| 364 | + public void runLive_transferToSelf_reopensTheCallerNotANamesake() { |
| 365 | + TestLlm callerLlm = |
| 366 | + createTestLlm( |
| 367 | + Flowable.just(transferTo("shared")), |
| 368 | + Flowable.just(createTextLlmResponse("caller reopened"))); |
| 369 | + TestLlm namesakeLlm = TestLlm.create(createTextLlmResponse("namesake ran")); |
| 370 | + |
| 371 | + ImmutableList<Event> events = runLive(rootWithNamesakeTargets(callerLlm, namesakeLlm)); |
| 372 | + |
| 373 | + assertThat(simplifyEvents(events)).contains("shared: caller reopened"); |
| 374 | + assertThat(callerLlm.getRequests()).hasSize(2); |
| 375 | + assertThat(namesakeLlm.getRequests()).isEmpty(); |
| 376 | + } |
| 377 | + |
245 | 378 | @Test |
246 | 379 | public void testAutoToAuto() { |
247 | 380 | Content transferCallContent = Content.fromParts(createTransferCallPart("sub_agent_1")); |
@@ -953,4 +1086,22 @@ private List<Event> runRunner(Runner runner, InvocationContext invocationContext |
953 | 1086 | .toList() |
954 | 1087 | .blockingGet(); |
955 | 1088 | } |
| 1089 | + |
| 1090 | + private static ImmutableList<Event> runLive(LlmAgent root) { |
| 1091 | + List<Event> events = new ArrayList<>(); |
| 1092 | + runLive(root, events); |
| 1093 | + return ImmutableList.copyOf(events); |
| 1094 | + } |
| 1095 | + |
| 1096 | + /** Runs {@code root} live after one user message, adding each event to {@code events}. */ |
| 1097 | + private static void runLive(LlmAgent root, List<Event> events) { |
| 1098 | + Runner runner = new InMemoryRunner(root, "test"); |
| 1099 | + LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); |
| 1100 | + liveRequestQueue.content(Content.fromParts(Part.fromText("hi"))); |
| 1101 | + runner |
| 1102 | + .runLive(newSession(runner), liveRequestQueue, RunConfig.builder().build()) |
| 1103 | + .doOnNext(events::add) |
| 1104 | + .ignoreElements() |
| 1105 | + .blockingAwait(); |
| 1106 | + } |
956 | 1107 | } |
0 commit comments