Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,18 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./tools/github-actions/setup-deps
- name: Install ripgrep
shell: bash
run: |
sudo apt-get update
sudo apt-get install --yes ripgrep
# Use the pinned upstream binary to avoid Ubuntu mirror DNS failures.
archive="$RUNNER_TEMP/ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz"
curl --fail --location --retry 2 --connect-timeout 20 --max-time 180 \
--output "$archive" \
https://github.com/BurntSushi/ripgrep/releases/download/13.0.0/ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz
printf '%s %s\n' ee4e0751ab108b6da4f47c52da187d5177dc371f0f512a7caaec5434e711c091 "$archive" \
| sha256sum --check
tar -xzf "$archive" -C "$RUNNER_TEMP" ripgrep-13.0.0-x86_64-unknown-linux-musl/rg
sudo install -m 0755 "$RUNNER_TEMP/ripgrep-13.0.0-x86_64-unknown-linux-musl/rg" /usr/local/bin/rg
rg --version
- run: make tools
- run: ./mvnw -B install
build:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.time.Duration;
import java.util.ArrayList;
Expand Down Expand Up @@ -201,11 +203,23 @@ void queuedParallelToolDoesNotStartAfterLeaseLoss() throws Exception {
assertEquals(0, delegateCalls.get(), "queued delegate must not start after guard loss");
}

@Test
void activeCancellableToolGetsCancellationOnLeaseLossAndLateOutputIsNotMerged() throws Exception {
@ParameterizedTest
@ValueSource(booleans = { false, true })
void activeCancellableToolGetsCancellationOnLeaseLossAndLateOutputIsNotMerged(boolean delayStageReturn)
throws Exception {
ManualGuard guard = new ManualGuard();
CountDownLatch toolStarted = new CountDownLatch(1);
CountDownLatch toolCancelled = new CountDownLatch(1);
CompletableFuture<Void> releaseStage = new CompletableFuture<>();
if (!delayStageReturn) {
releaseStage.complete(null);
}
CompletableFuture<ToolResult> original = new CompletableFuture<>();
original.whenComplete((value, failure) -> {
if (original.isCancelled()) {
toolCancelled.countDown();
}
});
Tool tool = new Tool() {
@Override
public ToolDefinition definition() {
Expand All @@ -215,6 +229,8 @@ public ToolDefinition definition() {
@Override
public CompletionStage<ToolResult> execute(ToolCall call, ToolContext context) {
toolStarted.countDown();
// Model a tool that has started but has not handed its stage to the node.
releaseStage.join();
return original;
}
};
Expand All @@ -229,14 +245,23 @@ public CompletionStage<ToolResult> execute(ToolCall call, ToolContext context) {
throw new CompletionException(ex);
}
}, invocationExecutor);
assertTrue(toolStarted.await(5, TimeUnit.SECONDS), "tool must start before ownership is lost");
guard.lose("active async tool lost ownership");

ExecutionException error = assertThrows(ExecutionException.class, () -> result.get(5, TimeUnit.SECONDS));
LeaseLostException loss = assertRootLeaseLoss(error);
assertEquals("active async tool lost ownership", loss.getReason());
assertTrue(original.isCancelled(), "lease loss must request original stage cancellation");
assertTrue(!original.complete(ToolResult.text("late")), "late output must not be published");
try {
assertTrue(toolStarted.await(5, TimeUnit.SECONDS), "tool must start before ownership is lost");
guard.lose("active async tool lost ownership");

ExecutionException error = assertThrows(ExecutionException.class, () -> result.get(5, TimeUnit.SECONDS));
LeaseLostException loss = assertRootLeaseLoss(error);
assertEquals("active async tool lost ownership", loss.getReason());
// Lease loss can end the node before the tool returns its stage. Once the
// stage is available, wait for cancellation propagation before inspecting it.
releaseStage.complete(null);
assertTrue(toolCancelled.await(5, TimeUnit.SECONDS), "original stage must receive cancellation");
assertTrue(original.isCancelled(), "lease loss must request original stage cancellation");
assertTrue(!original.complete(ToolResult.text("late")), "late output must not be published");
}
finally {
releaseStage.complete(null);
}
}

private static AgentToolNode.Builder builder() {
Expand Down
Loading