8000 fix: load artifacts from all parallel tool responses by GCSimba · Pull Request #837 · google/adk-kotlin · GitHub
[go: up one dir, main page]

Skip to content
10000

fix: load artifacts from all parallel tool responses - #837

Open
GCSimba wants to merge 1 commit into
google:mainfrom
GCSimba:codex/fix-parallel-artifact-loading
Open

GCSimba wants to merge 1 commit into
google:mainfrom
GCSimba:codex/fix-parallel-artifact-loading

Conversation

@GCSimba
@GCSimba GCSimba commented Oct 31, 2026 •
Copy link
Copy Markdown
Contributor

When another tool precedes load_artifacts in the same parallel call batch, the loader returns the requested filenames but omits their contents from the next model request because it only inspects the first response part.

Scan every part of the latest response message and load artifacts from all matching responses, preserving their order and skipping missing files. This matches Python ADK's handling of batched responses.

Adds five regressions to the existing LoadArtifactsToolTest:

  • runner flows with the artifact call before and after another tool;
  • two batched runner flows that load a user:-scoped artifact saved in another session, requested with and without the user: prefix;
  • multiple load responses interleaved with text, unrelated responses, and missing files.

The runner flows check that the answering model's last content is exactly the loaded artifact. The request-processing test also checks that the original request remains unchanged.

Validation on JDK 21, at b4d321b (rebased onto b961c9e):

  • LoadArtifactsToolTest: 19/19 JVM and 19/19 Android host tests passed, with no skips. With LoadArtifactsTool.kt from main, 4 of the 19 fail: the after-another-tool flow, both user: flows, and the multiple-responses test. The before-another-tool flow still passes, since the old loader read only the first response part.
  • Repository-wide build passed on Ubuntu 24.04 (WSL2): 500 tasks (458 executed; the other 42 were up to date from the targeted runs in the same checkout). 10,584 tests passed with no failures; 18 skipped XML records are live Gemini/Firebase or local-model tests without credentials/models. No test exclusions or test-disable flags were used, integration retries remained at repository defaults, and the build left the working tree clean.
  • git diff --check passed. ktfmt --google-style (0.65) would only add trailing commas to the new lines, as it would throughout the existing files.

After rebasing onto e863fac (now 54a7108, same diff), LoadArtifactsToolTest passed again on the new base: 19/19 JVM and 19/19 Android host. With LoadArtifactsTool.kt from the new main, the same 4 tests fail.

The Linux runs used -PjdkVersion=21, -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxMetaspaceSize=1g", -Dorg.gradle.parallel=false, --max-workers=2, --no-daemon, --stacktrace, and --continue. On-device tests, real backend/model execution, and the JDK 17/25 matrix were not run locally.

This change was drafted with AI assistance (GitHub Copilot); I reviewed the diff and the validation results above.

@aravindmurali22 aravindmurali22 self-assigned this Nov 2, 2026
@aravindmurali22 aravindmurali22 added the need attention New issue awaiting maintainer review or initial triage label Nov 2, 2026
Comment on lines +46 to +48
mapOf(
"artifact_names" to Schema(type = Type.ARRAY, items = Schema(type = Type.STRING)),
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert the unrelated formatting change in declaration() (and the trailing-comma edits on untouched tests in LoadArtifactsToolTest.kt lines 97–386) so the diff stays focused on processLoadArtifactsFunctionCall and the new tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b4d321b: reverted the declaration() rewrap and the trailing-comma edits to the existing tests. The diff now only touches processLoadArtifactsFunctionCall, which keeps its original Content(...) layout inside the new loop, plus the imports and new tests in LoadArtifactsToolTest.kt.

Comment on lines +465 to +531

private suspend fun assertArtifactReachesAnsweringModel(loadArtifactsFirst: Boolean) {
val artifacts = InMemoryArtifactService()
val filename = "invoice.txt"
val payload = "Invoice total is 1234.56 USD"
artifacts.saveArtifact(
SessionKey("artifact_app", "user1", "session1"),
filename,
Part(text = payload),
)
val currencyCall = Part(functionCall = FunctionCall("get_currency", id = "currency-1"))
val artifactCall =
Part(
functionCall =
FunctionCall(
"load_artifacts",
args = mapOf("artifact_names" to listOf(filename)),
id = "artifact-1",
),
)
val orderedCalls =
if (loadArtifactsFirst) listOf(artifactCall, currencyCall)
else listOf(currencyCall, artifactCall)
var calls = 0
var answerRequest: LlmRequest? = null
val model =
DummyModel("artifact-model") { request ->
calls++
if (calls == 1) {
flowOf(LlmResponse(content = Content(role = Role.MODEL, parts = orderedCalls)))
} else {
answerRequest = request
flowOf(LlmResponse(content = modelMessage("Finished")))
}
}
val agent =
LlmAgent(
name = "invoice_agent",
model = model,
tools =
listOf(
DummyTool(name = "get_currency", declares = true) { _, _ ->
mapOf("currency" to "USD")
},
LoadArtifactsTool(),
),
)
val runner =
InMemoryRunner(agent = agent, appName = "artifact_app", artifactService = artifacts)
try {
val events =
runner.runAsync("user1", "session1", newMessage = userMessage("Read the invoice")).toList()
val responses = events.flatMap { it.functionResponses() }
assertEquals(orderedCalls.map { it.functionCall!!.name }, responses.map { it.name })
val artifactResponse = responses.single { it.name == "load_artifacts" }
assertEquals(listOf(filename), artifactResponse.response["artifact_names"])
val nextRequest = assertNotNull(answerRequest)
assertEquals(2, calls)
assertTrue(
nextRequest.contents.flatMap { it.parts }.any { it.text == payload },
"A successful load_artifacts response must supply the file to the answering model; " +
"loadArtifactsFirst=$loadArtifactsFirst",
)
} finally {
runner.close()
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In assertArtifactReachesAnsweringModel, please also assert that nextRequest.contents.last() is the expected Content(role = Role.USER, parts = listOf(Part(text = "Artifact $filename is:"), Part(text = payload))) (and add a case or assertion for a "user:"-scoped cross-session artifact in a batched response).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b4d321b: assertArtifactReachesAnsweringModel now asserts that nextRequest.contents.last() is Content(role = Role.USER, parts = listOf(Part(text = "Artifact $filename is:"), Part(text = payload))). Two new batched cases save the artifact as user:invoice.txt in another session and call get_currency before load_artifacts: one requests user:invoice.txt, the other requests invoice.txt and relies on the user: fallback in loadArtifact. With LoadArtifactsTool.kt from main, both fail, as do the existing after-another-tool and multiple-responses tests; the before-another-tool case still passes.

@aravindmurali22 aravindmurali22 left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing batched load_artifacts responses and adding the regression tests, @GCSimba! Left two small comments on reverting the unrelated trailing-comma formatting churn and tightening the assertion in LoadArtifactsToolTest, please also rebase onto the latest main.

@aravindmurali22 aravindmurali22 added waiting on author Awaiting response from author. Failing that, maintainers will eventually closed it as stale. and removed need attention New issue awaiting maintainer review or initial triage labels Nov 2, 2026
@GCSimba
GCSimba force-pushed the codex/fix-parallel-artifact-loading branch from 433fbd2 to b4d321b Compare November 3, 2026 06:08
@GCSimba
GCSimba commented Nov 3, 2026
Copy link
Copy Markdown
Contributor Author

Thanks for the review, @aravindmurali22! Rebased onto the latest main and addressed both comments in b4d321b.

@aravindmurali22
Copy link
Copy Markdown
Contributor

Thank you for the quick response @GCSimba , could you please rebase your branch onto the latest main once again? Thanks!

@aravindmurali22 aravindmurali22 added google support Escalated to Google internal support or engineering team and removed waiting on author Awaiting response from author. Failing that, maintainers will eventually closed it as stale. labels Nov 3, 2026
@GCSimba
GCSimba force-pushed the codex/fix-parallel-artifact-loading branch from b4d321b to 54a7108 Compare November 4, 2026 03:33
@GCSimba
GCSimba commented Nov 4, 2026
Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main (e863fac) in 54a7108. No conflicts and the diff is unchanged; LoadArtifactsToolTest passes again on JVM and Android host (19/19 each).

@aravindmurali22
Copy link
Copy Markdown
Contributor

@GCSimba Here as well

  • Please check and resolve the failing GitHub Actions checks.
  • Because this repo sees daily commits, please monitor updates and keep your branch rebased against main until merge.

Appreciate your help with this!

@GCSimba
GCSimba force-pushed the codex/fix-parallel-artifact-loading branch from 54a7108 to a963450 Compare November 5, 2026 17:53
@GCSimba
GCSimba commented Nov 5, 2026
Copy link
Copy Markdown
Contributor Author

Rebased onto e665796 again; the diff is unchanged. LoadArtifactsToolTest passes on the JVM and Android host tests (19/19 each), and still fails 4 of the 19 with LoadArtifactsTool.kt from main.

About the failing check: the last validation run failed only in KtorChatCompletionsClientTest.create_timeoutClosesTheConnectionWhileTheResponseStalls (integrations/openai), which this PR does not touch. It looks like a flaky test rather than something this PR can fix: of the 85 completed validation runs in this repo since Nov 4, the 21 that failed all failed in one of four timing-sensitive tests, 19 of them on other PRs:

Test Failed runs
LiveRoutesTest.runLive_underTheAppsOwnWebSockets_closesAnOversizedFrameWithTooBig 7
KtorChatCompletionsClientTest.create_timeoutClosesTheConnectionWhileTheResponseStalls 6
McpToolsetIntegrationTest.run_unresponsiveServer_timesOutThenThrowsAfterRetries 4
WorkflowHitlTest.runNode_waitingCallCancelledWithTheFirst_doesNotRunTheChild 4
Failed runs

If the new run hits one of these again, could you re-run the failed job? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

google support Escalated to Google internal support or engineering team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0