Skip to content

[FEATURE] Port SaveFilesAsArtifactsPlugin from adk-python #1411

Description

@svetanis

Please make sure you read the contribution guide and file the issues in the right place.
Contribution guide.

🔴 Required Information

Is your feature request related to a specific problem?

adk-python deprecated save_input_blobs_as_artifacts in favour of SaveFilesAsArtifactsPlugin.
adk-java has the parameter but not the plugin, so RunConfig.saveInputBlobsAsArtifacts(true) is the
only way to keep uploaded bytes out of every later LLM request — and it loses two things the plugin
provides.

1. The uploaded file name is discarded. Runner names every artifact
artifact_{invocationId}_{index} and never reads Blob.displayName.

// Runner, in the blob-offload loop
String fileName = "artifact_" + invocationContext.invocationId() + "_" + i;

An upload of report.pdf is stored as artifact_<invocationId>_1, so anything that later loads it
must work from an opaque id that appears nowhere but the placeholder text.

2. Nothing is reported to the session. The user event carries a state delta only, so
EventActions.artifactDelta stays empty and the session's artifact bookkeeping never records the
upload.

// Runner.appendNewMessageToSession — no artifactDelta is set on this event
if (stateDelta != null && !stateDelta.isEmpty()) {
  eventBuilder.actions(EventActions.builder().stateDelta(new ConcurrentHashMap<>(stateDelta)).build());
}

Both behaviours live in the plugin.

Describe the Solution You'd Like

SaveFilesAsArtifactsPlugin in com.google.adk.plugins, registered on a Runner like any other
plugin. For each inlineData part of the incoming user message it would:

  • save the part to the configured BaseArtifactService, named from Blob.displayName, falling back
    to artifact_{invocationId}_{index} when the blob carries no name;
  • replace it with [Uploaded Artifact: "<name>"] in the message that reaches the model and is
    appended to the session, matching adk-python's wording;
  • report the saved versions through EventActions.artifactDelta;
  • on a failed save, keep the original part and log, without failing the invocation — as in
    adk-python, the log is the only signal, so an unreachable artifact service degrades silently to no
    offload.

attach_file_reference would not be ported: it needs
get_artifact_version(...).canonical_uri, which has no equivalent on BaseArtifactService, so it
would require an SPI change across every implementation. Separate change.

Impact on your work

Applications wanting adk-python's behaviour must reimplement it in application code, where it will
drift from upstream. Not blocking, and no timeline — this is a parity gap, not an outage.

Willingness to contribute

Yes. A PR follows immediately after this issue: one new plugin class plus a small package-private helper, with tests. No existing file is modified, and the only new public surface is the plugin class itself.


🟡 Recommended Information

Describe Alternatives You've Considered

Teach the existing flag to read Blob.displayName and set artifactDelta. Smaller diff, but it
adds behaviour to the parameter adk-python is steering users away from, and adk-java would still have
no plugin — widening the divergence rather than closing it.

Implement the plugin in application code. This works using public API only, and is how the
behaviour below was verified. It is per-application boilerplate for something a plugin surface exists
to ship once.

Proposed API / Implementation

Registration needs no new API — the existing surfaces already accept it:

Runner runner = Runner.builder()
    .agent(agent)
    .appName("my-app")
    .artifactService(new InMemoryArtifactService())
    .sessionService(new InMemorySessionService())
    .plugins(new SaveFilesAsArtifactsPlugin())   // also App.Builder.plugins(...) and
    .build();                                    // InMemoryRunner(agent, appName, plugins)

Two hooks, both already wired into the runtime:

public class SaveFilesAsArtifactsPlugin extends BasePlugin {

  @Override
  public Maybe<Content> onUserMessageCallback(InvocationContext ctx, Content userMessage) {
    // save each inlineData part, swap it for [Uploaded Artifact: "<name>"],
    // stash {fileName: version} under a temp: state key, return the rebuilt Content
  }

  @Override
  public Maybe<Content> beforeAgentCallback(BaseAgent agent, CallbackContext callbackContext) {
    // drain the stash into callbackContext.eventActions().artifactDelta(), return Maybe.empty()
  }
}

The state hand-off exists because onUserMessageCallback runs before any EventActions exists, so
the saved versions cannot be reported from there. adk-python solves it the same way.

Additional Context

Same upload, same runner, differing only in which mechanism performs the offload:

--- Run A: RunConfig.saveInputBlobsAsArtifacts(true) ---
  user attached                     : blob with displayName="report.pdf"
  artifacts stored in the session   : artifact_e-eb41628a-9812-43a0-88d0-7b121706190b_1
  EventActions.artifactDelta        : (none)
  message appended to the session   :
                                      - text: "read the attachment"
                                      - text: "Uploaded file: artifact_e-eb41628a-…_1. It has been saved to the artifacts"

--- Run B: SaveFilesAsArtifactsPlugin (the proposed port) ---
  user attached                     : blob with displayName="report.pdf"
  artifacts stored in the session   : report.pdf
  EventActions.artifactDelta        : {report.pdf=0}
  message appended to the session   :
                                      - text: "read the attachment"
                                      - text: "[Uploaded Artifact: "report.pdf"]"

Both offload the payload losslessly and both show the model a placeholder instead of the bytes — the
gap is the file name and the bookkeeping, not the offload.

Observed on 1.7.2-SNAPSHOT, Windows 11 (not OS-specific), with a stub model and with
gemini-3.5-flash; both mechanisms run before the model call.

Activity

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

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions