Repository navigation
Description
🔴 Required Information
Describe the Bug:
An artifact saved with callback_context.save_artifact(...) inside a before_agent_callback or after_agent_callback is written to the artifact service, but its artifact_delta is never recorded in any session event.
BaseAgent._handle_agent_callbacks (src/google/adk/agents/base_agent.py) only emits an event when the callback returns content or when callback_context.state.has_delta() is true. A callback that only saves an artifact (and returns None) changes callback_context._event_actions.artifact_delta, but that is dropped because no event carries it.
Consequences: the session history has no record of the artifact, so Runner.rewind_async (which computes artifact changes from artifact_delta) will not revert it, and anything that surfaces artifacts from events (web UI, A2A) never sees it. The same artifact saved from a before_model_callback or a tool is recorded, and the workflow node runner already checks both state and artifact deltas.
Steps to Reproduce:
pip install google-adk==2.11.0- Run the snippet below.
Expected Behavior:
artifact keys: ['report.txt']
artifact_delta in session events: [{'report.txt': 0}]
Observed Behavior:
artifact keys: ['report.txt']
artifact_delta in session events: []
Environment Details:
- ADK Library Version: 2.11.0 (also reproduced on
mainat 3d11f9f) - Desktop OS: macOS
- Python Version: 3.12.13
Model Information:
- Are you using LiteLLM: No
- Which model is being used: N/A (no model call needed to reproduce)
🟡 Optional Information
Minimal Reproduction Code:
import asyncio
from google.adk.agents import BaseAgent
from google.adk.agents.callback_context import CallbackContext
from google.adk.artifacts import InMemoryArtifactService
from google.adk.events import Event
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
async def save_report(callback_context: CallbackContext):
await callback_context.save_artifact(
"report.txt", types.Part.from_text(text="hello")
)
class EchoAgent(BaseAgent):
async def _run_async_impl(self, ctx):
yield Event(
author=self.name,
invocation_id=ctx.invocation_id,
content=types.Content(role="model", parts=[types.Part(text="done")]),
)
async def main():
artifacts = InMemoryArtifactService()
runner = Runner(
app_name="app",
agent=EchoAgent(name="agent", before_agent_callback=save_report),
session_service=InMemorySessionService(),
artifact_service=artifacts,
)
session = await runner.session_service.create_session(
app_name="app", user_id="u"
)
async for _ in runner.run_async(
user_id="u",
session_id=session.id,
new_message=types.Content(role="user", parts=[types.Part(text="hi")]),
):
pass
session = await runner.session_service.get_session(
app_name="app", user_id="u", session_id=session.id
)
print("artifact keys:", await artifacts.list_artifact_keys(
app_name="app", user_id="u", session_id=session.id))
print("artifact_delta in session events:",
[e.actions.artifact_delta for e in session.events
if e.actions.artifact_delta])
asyncio.run(main())How often has this issue occurred?:
- Always (100%)
Proposed fix: In _handle_agent_callbacks, also emit the event when callback_context._event_actions.artifact_delta is non-empty, not only when state has a delta. I have a PR ready with unit tests for both callbacks.
Activity
llalitkumarrr commentedon Oct 13, 2026
Hello @AkshayShah03,
Thank you for the detailed report and your contribution. Your pull request is currently under review and will be merged once the reviewer approves the changes. We will reach out if we need any further details. Thank you for your patience in the meantime.
fix: record artifact_delta from before and after agent callbacks
fix(agent): record artifact-only deltas from before/after agent callb…