fix: align agent timeline event contract

This commit is contained in:
2026-05-26 12:20:33 +08:00
parent f58a57e5b8
commit 96562877cc
5 changed files with 55 additions and 3 deletions

View File

@ -81,8 +81,8 @@ class SessionProcessProjector:
root["summary"] = payload.get("reason") or "" root["summary"] = payload.get("reason") or ""
root["metadata"] = { root["metadata"] = {
**root.get("metadata", {}), **root.get("metadata", {}),
"plan_mode": payload.get("plan_mode"), "plan_mode": plan_mode,
"strategy": payload.get("strategy"), "strategy": strategy,
"node_ids": node_ids, "node_ids": node_ids,
"skill_queries": payload.get("skill_queries") or [], "skill_queries": payload.get("skill_queries") or [],
"selected_skill_names": payload.get("selected_skill_names") or [], "selected_skill_names": payload.get("selected_skill_names") or [],
@ -234,7 +234,6 @@ class SessionProcessProjector:
"task_id": task_id, "task_id": task_id,
"attempt_index": attempt_index, "attempt_index": attempt_index,
"timeline_type": "agent_progress", "timeline_type": "agent_progress",
"node_result": dict(item),
}, },
) )

View File

@ -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") 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"]["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"] assert planned_event["metadata"]["selected_skill_names"] == ["research-workflow"]
skill_event = next(event for event in projection["events"] if event["kind"] == "skill_selected") 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") node_event = next(event for event in projection["events"] if event["kind"] == "agent_finished")
assert node_event["metadata"]["timeline_type"] == "agent_progress" 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") evidence_event = next(event for event in projection["events"] if event["kind"] == "task_result_ready")
assert evidence_event["metadata"]["timeline_type"] == "result" 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" 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: def test_process_projection_exposes_ephemeral_guidance_artifacts(tmp_path: Path) -> None:
session = SessionManager(tmp_path) session = SessionManager(tmp_path)
run_store = RunMemoryStore(tmp_path / "memory" / "runs") run_store = RunMemoryStore(tmp_path / "memory" / "runs")

View File

@ -201,6 +201,28 @@ describe('buildTaskTimelineCards', () => {
expect(cards.map((card) => card.id)).not.toContain('run-research:fallback-progress'); 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', () => { it('sorts invalid timestamps after valid timestamps while preserving insertion order', () => {
const task = makeTask(); const task = makeTask();
const processEvents: ProcessEvent[] = [ const processEvents: ProcessEvent[] = [

View File

@ -95,6 +95,7 @@ function cardTypeForEvent(event: ProcessEvent): TaskTimelineCardType | null {
return 'agent_team'; return 'agent_team';
case 'agent_handoff': case 'agent_handoff':
return 'agent_handoff'; return 'agent_handoff';
case 'agent_finished':
case 'run_progress': case 'run_progress':
case 'run_finished': case 'run_finished':
return 'agent_progress'; return 'agent_progress';

View File

@ -455,6 +455,7 @@ export type ProcessEventKind =
| 'tool_call_started' | 'tool_call_started'
| 'tool_call_finished' | 'tool_call_finished'
| 'agent_team_created' | 'agent_team_created'
| 'agent_finished'
| 'agent_handoff' | 'agent_handoff'
| 'task_result_ready' | 'task_result_ready'
| 'task_acceptance_recorded' | 'task_acceptance_recorded'