fix(a2a): include assistant turns in conversation history - #508
Conversation
Greptile SummaryThis PR reconstructs prior assistant turns from persisted A2A response artifacts so follow-up tasks receive complete conversation history.
|
| Filename | Overview |
|---|---|
| crates/aura-web-server/src/a2a/agent_executor.rs | Reconstructs assistant history from response/final artifacts, requests artifacts during task listing, and reorders prior exchanges oldest-first with focused unit coverage. |
| crates/aura-web-server/tests/a2a_test.rs | Adds an end-to-end sequential-task test proving a follow-up can recall a value present only in the prior assistant reply, plus reusable A2A test helpers. |
Sequence Diagram
sequenceDiagram
participant Client
participant Handler as A2A Handler
participant Store as Task Store
participant Executor
participant Agent
Client->>Handler: Send first user message
Handler->>Store: Persist task prompt
Handler->>Executor: Execute task
Executor->>Agent: Stream prompt with prior history
Agent-->>Executor: Response chunks and final response
Executor-->>Handler: Response/final artifacts
Handler->>Store: Persist artifacts
Client->>Handler: Send follow-up with contextId
Handler->>Executor: Execute follow-up
Executor->>Store: List context tasks with artifacts
Executor->>Executor: Rebuild user and assistant turns
Executor->>Agent: Stream follow-up with complete history
Reviews (1): Last reviewed commit: "fix(a2a): include assistant turns in con..." | Re-trigger Greptile
Shearerbeard
left a comment
There was a problem hiding this comment.
Approving for now but i think after we flesh out park/reify #271 and get closer to task attach/detach this will have the same generic home + backend for all agent initializations - not just specific A2A
A follow-up message on an existing contextId was given the prior user turns but never the agent's own replies, so it answered as if it had never spoken. get_history_for_context rebuilt history from Task::history alone. The upstream request handler writes that field once, with the prompt, and never appends to it; the agent's reply is streamed out as artifacts instead, leaving the Role::Agent arm of convert_a2a_msg_to_aura unreachable. The same call also passed include_artifacts: Some(false), so the reply was never fetched, and sorted the context's tasks newest-first, which would have replayed a multi-task exchange backwards. History now rebuilds the assistant turn from the artifacts the client saw: the streamed "response" chunks concatenated with no separator, falling back to "final" for a reply that only arrived as a final response. Tool-call, reasoning, and scratchpad artifacts are skipped. Tasks replay oldest-first, and a task already carrying a ROLE_AGENT message keeps it rather than gaining a second one. The two artifact ids are now module consts shared with the sites that emit them, so reader and writer cannot drift apart. A2A clients still see only the prompt in task.history. The upstream handler owns every task write and carries a stale in-memory task across events, so writing the agent turn back would either race it or have to go through SharedTaskStore, which conflicts with the Redis store's terminal-immutability rule. Reconstructing on read works with both stores and touches no write path. test_sequential_tasks_share_the_assistant_turn covers the half that test_sequential_tasks_share_context cannot. It routes a per-run secret in through a forwarded header that the agent reports, then sends the follow-up without that header so the tool cannot surface the value again, and asserts the follow-up made no tool call at all, leaving the prior assistant turn as the only possible source. Fixes: #368
e7c3c93 to
c579bfb
Compare
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
A follow-up message on an existing contextId was given the prior user turns but never the agent's own replies, so it answered as if it had never spoken.
get_history_for_context rebuilt history from Task::history alone. The upstream request handler writes that field once, with the prompt, and never appends to it; the agent's reply is streamed out as artifacts instead, leaving the Role::Agent arm of convert_a2a_msg_to_aura unreachable. The same call also passed include_artifacts: Some(false), so the reply was never fetched, and sorted the context's tasks newest-first, which would have replayed a multi-task exchange backwards.
History now rebuilds the assistant turn from the artifacts the client saw: the streamed "response" chunks concatenated with no separator, falling back to "final" for a reply that only arrived as a final response. Tool-call, reasoning, and scratchpad artifacts are skipped. Tasks replay oldest-first, and a task already carrying a ROLE_AGENT message keeps it rather than gaining a second one. The two artifact ids are now module consts shared with the sites that emit them, so reader and writer cannot drift apart.
A2A clients still see only the prompt in task.history. The upstream handler owns every task write and carries a stale in-memory task across events, so writing the agent turn back would either race it or have to go through SharedTaskStore, which conflicts with the Redis store's terminal-immutability rule. Reconstructing on read works with both stores and touches no write path.
test_sequential_tasks_share_the_assistant_turn covers the half that test_sequential_tasks_share_context cannot. It routes a per-run secret in through a forwarded header that the agent reports, then sends the follow-up without that header so the tool cannot surface the value again, and asserts the follow-up made no tool call at all, leaving the prior assistant turn as the only possible source.
Fixes: #368