Skip to content

Commit be4d7aa

Browse files
CharlesthebirdclaudeEItanya
authored
fix: archive a parked question before its reply (#2616)
Before there was a bug where the agent question disappears from the chat: https://github.com/user-attachments/assets/3e422b25-cb21-4b8b-b21c-52f2e7227e20 After, this is now stable across refreshes: https://github.com/user-attachments/assets/0ce5a581-7cf4-4693-8276-55949b31a55e --- *🤖 written by Claude (start)* ## Changelog An agent's question no longer disappears from the transcript once you answer it: a parked task holds the question in its status, which the reply replaces, so it is now archived as history first. ## Testing 1. Ask an agent to ask you a question, and answer it through the prompt. 2. Reload the page — the question is still there, above your answer. --- *🤖 written by Claude (end)* --------- Signed-off-by: Nicholas Bucher <behappy54321@gmail.com> Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Eitan Yarmush <eitan.yarmush@solo.io>
1 parent 8bd83e4 commit be4d7aa

4 files changed

Lines changed: 106 additions & 1 deletion

File tree

go/core/internal/database/client_agent_instance_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,49 @@ func TestConcurrentAgentInstanceMessageReplay(t *testing.T) {
208208
}
209209
}
210210

211+
func TestAgentInstanceReplyArchivesStatusMessageAtomically(t *testing.T) {
212+
db := setupTestDB(t)
213+
ctx := context.Background()
214+
if _, err := db.Exec(ctx, `
215+
INSERT INTO a2a_context (id, namespace, user_id)
216+
VALUES ('instance-1', 'team-a', 'alice');
217+
INSERT INTO agent_instance (id, namespace, user_id, request_id, context_id, state, data)
218+
VALUES ('instance-1', 'team-a', 'alice', 'request-1', 'instance-1', 'READY', '\\x00')
219+
`); err != nil {
220+
t.Fatal(err)
221+
}
222+
client := NewClient(db)
223+
asked := &a2a.Message{ID: "message-1", Role: a2a.MessageRoleUser, TaskID: "task-1", ContextID: "instance-1"}
224+
question := &a2a.Message{ID: "question-1", Role: a2a.MessageRoleAgent, TaskID: "task-1", ContextID: "instance-1"}
225+
parked := &a2a.Task{
226+
ID: "task-1", ContextID: "instance-1", History: []*a2a.Message{asked},
227+
Status: a2a.TaskStatus{State: a2a.TaskStateInputRequired, Message: question},
228+
}
229+
if _, _, err := client.CreateAgentInstanceTask(ctx, "instance-1", []byte("request-1"), parked); err != nil {
230+
t.Fatal(err)
231+
}
232+
233+
answer := &a2a.Message{ID: "answer-1", Role: a2a.MessageRoleUser, TaskID: "task-1", ContextID: "instance-1"}
234+
resumed := *parked
235+
resumed.History = []*a2a.Message{asked, question, answer}
236+
resumed.Status = a2a.TaskStatus{State: a2a.TaskStateSubmitted}
237+
if err := client.StoreAgentInstanceTaskEvent(ctx, "instance-1", &resumed, answer, nil); err != nil {
238+
t.Fatal(err)
239+
}
240+
241+
got, err := client.GetAgentInstanceTask(ctx, "instance-1", "task-1")
242+
if err != nil {
243+
t.Fatal(err)
244+
}
245+
ids := make([]string, 0, len(got.History))
246+
for _, message := range got.History {
247+
ids = append(ids, message.ID)
248+
}
249+
if strings.Join(ids, ",") != "message-1,question-1,answer-1" {
250+
t.Fatalf("history = %v, want the question between the message it answers and its own answer", ids)
251+
}
252+
}
253+
211254
func TestAgentInstanceCheckpointRetainsRecordedBoundary(t *testing.T) {
212255
db := setupTestDB(t)
213256
ctx := context.Background()

go/core/internal/database/client_postgres.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,12 +854,19 @@ func (c *postgresClient) InterruptActiveAgentInstanceTask(ctx context.Context, i
854854
func (c *postgresClient) StoreAgentInstanceTaskEvent(ctx context.Context, instanceID string, task *a2a.Task, event a2a.Event, snapshot *dbpkg.AgentInstanceTaskSnapshot) error {
855855
err := c.withTx(ctx, func(q *dbgen.Queries) error {
856856
var sequence int64
857+
var replacedStatusMessage *a2a.Message
857858
if task != nil {
858859
if row, err := q.GetAgentInstanceTask(ctx, dbgen.GetAgentInstanceTaskParams{ContextID: instanceID, ID: string(task.ID)}); err == nil {
859860
previous, err := unmarshalAgentInstanceTask(row.Data)
860861
if err != nil {
861862
return err
862863
}
864+
// A reply replaces the current status message, so archive both atomically.
865+
if _, ok := event.(*a2a.Message); ok && previous.Status.Message != nil {
866+
message := *previous.Status.Message
867+
message.TaskID, message.ContextID = task.ID, task.ContextID
868+
replacedStatusMessage = &message
869+
}
863870
if len(previous.History) > 0 {
864871
sequence, err = storeAgentInstanceTaskMessages(ctx, q, instanceID, string(task.ID), previous.History)
865872
if err != nil {
@@ -884,6 +891,9 @@ func (c *postgresClient) StoreAgentInstanceTaskEvent(ctx context.Context, instan
884891
}
885892
}
886893
messages := agentInstanceTaskEventMessages(task, event)
894+
if replacedStatusMessage != nil {
895+
messages = append([]*a2a.Message{replacedStatusMessage}, messages...)
896+
}
887897
if len(messages) > 0 {
888898
var err error
889899
sequence, err = storeAgentInstanceTaskMessages(ctx, q, instanceID, string(event.TaskInfo().TaskID), messages)

go/core/v2/a2agateway/gateway.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,15 @@ func (g *Gateway) prepareReply(ctx context.Context, instance *apiv1alpha1.AgentI
517517
}
518518
message.ContextID = stored.ContextID
519519
attempt := *stored
520-
attempt.History = append(append([]*a2atype.Message{}, stored.History...), message)
520+
attempt.History = append([]*a2atype.Message{}, stored.History...)
521+
if question := stored.Status.Message; question != nil {
522+
if question.ID == "" {
523+
return nil, a2atype.NewError(a2atype.ErrInternalError, "stored task status message has no ID")
524+
}
525+
question.TaskID, question.ContextID = stored.ID, stored.ContextID
526+
attempt.History = append(attempt.History, question)
527+
}
528+
attempt.History = append(attempt.History, message)
521529
now := time.Now()
522530
attempt.Status = a2atype.TaskStatus{State: a2atype.TaskStateSubmitted, Timestamp: &now}
523531
if err := g.store.StoreAgentInstanceTaskEvent(ctx, instance.GetId(), &attempt, message, nil); err != nil {

go/core/v2/a2agateway/gateway_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,50 @@ func TestGatewayContinuesInputRequiredTask(t *testing.T) {
392392
}
393393
}
394394

395+
func TestGatewayMovesInputRequiredMessageBeforeReply(t *testing.T) {
396+
question := a2atype.NewMessage(a2atype.MessageRoleAgent, a2atype.NewTextPart("Which database?"))
397+
waiting := &a2atype.Task{
398+
ID: "task-1", ContextID: gatewayTestID,
399+
Status: a2atype.TaskStatus{State: a2atype.TaskStateInputRequired, Message: question},
400+
}
401+
reply := a2atype.NewMessage(a2atype.MessageRoleUser, a2atype.NewTextPart("PostgreSQL"))
402+
reply.TaskID = waiting.ID
403+
store := &gatewayTestStore{task: waiting}
404+
gateway := &Gateway{store: store}
405+
406+
prepared, err := gateway.prepareReply(t.Context(), gatewayTestInstance(), &a2atype.SendMessageRequest{Message: reply})
407+
if err != nil {
408+
t.Fatal(err)
409+
}
410+
if len(prepared.task.History) != 2 || prepared.task.History[0] != question || prepared.task.History[1] != reply {
411+
t.Fatalf("history = %#v, want question followed by reply", prepared.task.History)
412+
}
413+
if len(store.stored) != 1 || store.stored[0] != reply {
414+
t.Fatalf("stored events = %#v, want one atomic reply update", store.stored)
415+
}
416+
if question.TaskID != waiting.ID || question.ContextID != waiting.ContextID {
417+
t.Fatalf("archived question = task %q context %q, want the task it was asked in", question.TaskID, question.ContextID)
418+
}
419+
}
420+
421+
func TestGatewayRejectsInputRequiredMessageWithoutID(t *testing.T) {
422+
waiting := &a2atype.Task{
423+
ID: "task-1", ContextID: gatewayTestID,
424+
Status: a2atype.TaskStatus{State: a2atype.TaskStateInputRequired, Message: &a2atype.Message{}},
425+
}
426+
store := &gatewayTestStore{task: waiting}
427+
gateway := &Gateway{store: store}
428+
reply := a2atype.NewMessage(a2atype.MessageRoleUser, a2atype.NewTextPart("PostgreSQL"))
429+
reply.TaskID = waiting.ID
430+
431+
if _, err := gateway.prepareReply(t.Context(), gatewayTestInstance(), &a2atype.SendMessageRequest{Message: reply}); err == nil {
432+
t.Fatal("prepareReply() succeeded with an unidentifiable status message")
433+
}
434+
if len(store.stored) != 0 {
435+
t.Fatalf("stored events = %#v, want no partial write", store.stored)
436+
}
437+
}
438+
395439
func TestGatewayClosesRuntimeAfterStreaming(t *testing.T) {
396440
instance := gatewayTestInstance()
397441
runtime := &gatewayTestRuntime{}

0 commit comments

Comments
 (0)