google / google/adk-java

[FEATURE] Port SaveFilesAsArtifactsPlugin from adk-python

Abierto
#1,411 1 comentario 0 reacciones 1 asignado Reclamado por @hemasekhar-p Ver en GitHub
needs review
Lenguaje dominante
Java
Estrellas
1.7k
Forks
420
Merge medio
4 d 12 h
PR fusionados (30 d)
31

Descripción

**Please make sure you read the contribution guide and file the issues in the right place.**
[Contribution guide.](https://google.github.io/adk-docs/contributing-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`.

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

An upload of `report.pdf` is stored as `artifact__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.

```java
// 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: ""]` 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:

```java
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:

```java
public class SaveFilesAsArtifactsPlugin extends BasePlugin {

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

@Override
public Maybe 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:

```text
--- 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.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.