From 96562877cc973e6137ee3bd2acebe8b125032b5c Mon Sep 17 00:00:00 2001 From: steven_li Date: Tue, 26 May 2026 12:20:33 +0800 Subject: [PATCH] fix: align agent timeline event contract --- .../beaver/services/process_service.py | 5 ++-- .../tests/unit/test_process_projection.py | 29 +++++++++++++++++++ .../frontend/lib/task-timeline.test.ts | 22 ++++++++++++++ app-instance/frontend/lib/task-timeline.ts | 1 + app-instance/frontend/types/index.ts | 1 + 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/app-instance/backend/beaver/services/process_service.py b/app-instance/backend/beaver/services/process_service.py index 596326c..dc79ab1 100644 --- a/app-instance/backend/beaver/services/process_service.py +++ b/app-instance/backend/beaver/services/process_service.py @@ -81,8 +81,8 @@ class SessionProcessProjector: root["summary"] = payload.get("reason") or "" root["metadata"] = { **root.get("metadata", {}), - "plan_mode": payload.get("plan_mode"), - "strategy": payload.get("strategy"), + "plan_mode": plan_mode, + "strategy": strategy, "node_ids": node_ids, "skill_queries": payload.get("skill_queries") or [], "selected_skill_names": payload.get("selected_skill_names") or [], @@ -234,7 +234,6 @@ class SessionProcessProjector: "task_id": task_id, "attempt_index": attempt_index, "timeline_type": "agent_progress", - "node_result": dict(item), }, ) diff --git a/app-instance/backend/tests/unit/test_process_projection.py b/app-instance/backend/tests/unit/test_process_projection.py index 7c47320..c579b20 100644 --- a/app-instance/backend/tests/unit/test_process_projection.py +++ b/app-instance/backend/tests/unit/test_process_projection.py @@ -137,6 +137,8 @@ def test_process_projection_maps_task_team_events(tmp_path: Path) -> None: planned_event = next(event for event in projection["events"] if event["kind"] == "task_planned") assert planned_event["metadata"]["timeline_type"] == "plan" + assert planned_event["metadata"]["plan_mode"] == "team" + assert planned_event["metadata"]["strategy"] == "sequence" assert planned_event["metadata"]["selected_skill_names"] == ["research-workflow"] skill_event = next(event for event in projection["events"] if event["kind"] == "skill_selected") @@ -149,6 +151,7 @@ def test_process_projection_maps_task_team_events(tmp_path: Path) -> None: node_event = next(event for event in projection["events"] if event["kind"] == "agent_finished") assert node_event["metadata"]["timeline_type"] == "agent_progress" + assert "node_result" not in node_event["metadata"] evidence_event = next(event for event in projection["events"] if event["kind"] == "task_result_ready") assert evidence_event["metadata"]["timeline_type"] == "result" @@ -209,6 +212,32 @@ def test_process_projection_maps_failed_task_team_events(tmp_path: Path) -> None assert node_event["metadata"]["timeline_type"] == "agent_progress" +def test_process_projection_uses_normalized_plan_metadata_defaults(tmp_path: Path) -> None: + session = SessionManager(tmp_path) + run_store = RunMemoryStore(tmp_path / "memory" / "runs") + session.append_message( + "web:test", + role="system", + event_type="task_execution_planned", + event_payload={ + "task_id": "task-1", + "attempt_index": 1, + "plan_mode": None, + "strategy": None, + }, + context_visible=False, + ) + + projection = SessionProcessProjector(session, run_store).project("web:test") + + root_run = next(run for run in projection["runs"] if run["run_id"] == "task:task-1:attempt:1") + assert root_run["metadata"]["plan_mode"] == "single" + assert root_run["metadata"]["strategy"] == "single" + planned_event = next(event for event in projection["events"] if event["kind"] == "task_planned") + assert planned_event["metadata"]["plan_mode"] == "single" + assert planned_event["metadata"]["strategy"] == "single" + + def test_process_projection_exposes_ephemeral_guidance_artifacts(tmp_path: Path) -> None: session = SessionManager(tmp_path) run_store = RunMemoryStore(tmp_path / "memory" / "runs") diff --git a/app-instance/frontend/lib/task-timeline.test.ts b/app-instance/frontend/lib/task-timeline.test.ts index 4d40378..e4c7cfe 100644 --- a/app-instance/frontend/lib/task-timeline.test.ts +++ b/app-instance/frontend/lib/task-timeline.test.ts @@ -201,6 +201,28 @@ describe('buildTaskTimelineCards', () => { expect(cards.map((card) => card.id)).not.toContain('run-research:fallback-progress'); }); + it('maps agent_finished events without timeline metadata to agent progress cards', () => { + const task = makeTask(); + const processEvents: ProcessEvent[] = [ + { + event_id: 'evt-agent-finished', + run_id: 'run-research', + parent_run_id: 'run-main', + kind: 'agent_finished', + actor_type: 'agent', + actor_id: 'research-agent', + actor_name: 'Research Agent', + text: 'Finished reading source documents.', + status: 'done', + created_at: '2026-05-26T10:02:00.000Z', + }, + ]; + + const cards = buildTaskTimelineCards({ task, processEvents }); + + expect(cards.find((card) => card.id === 'evt-agent-finished')?.type).toBe('agent_progress'); + }); + it('sorts invalid timestamps after valid timestamps while preserving insertion order', () => { const task = makeTask(); const processEvents: ProcessEvent[] = [ diff --git a/app-instance/frontend/lib/task-timeline.ts b/app-instance/frontend/lib/task-timeline.ts index dd2e125..ae3c9a9 100644 --- a/app-instance/frontend/lib/task-timeline.ts +++ b/app-instance/frontend/lib/task-timeline.ts @@ -95,6 +95,7 @@ function cardTypeForEvent(event: ProcessEvent): TaskTimelineCardType | null { return 'agent_team'; case 'agent_handoff': return 'agent_handoff'; + case 'agent_finished': case 'run_progress': case 'run_finished': return 'agent_progress'; diff --git a/app-instance/frontend/types/index.ts b/app-instance/frontend/types/index.ts index 7f52341..fcec5f3 100644 --- a/app-instance/frontend/types/index.ts +++ b/app-instance/frontend/types/index.ts @@ -455,6 +455,7 @@ export type ProcessEventKind = | 'tool_call_started' | 'tool_call_finished' | 'agent_team_created' + | 'agent_finished' | 'agent_handoff' | 'task_result_ready' | 'task_acceptance_recorded'