From 34b60e184274a129a92c550c24f905d3e23eee46 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Fri, 17 Apr 2026 19:10:48 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A8=20chore:=20return=20full=20brief?= =?UTF-8?q?=20data=20in=20task=20activities=20(#13914)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: return full brief data in task activities (LOBE-7266) The activity feed for tasks previously emitted a stripped `brief` row that concatenated `resolvedAction` and `resolvedComment` and omitted everything BriefCard needs (taskId, topicId, agentId, cronJobId, agents, actions, artifacts, readAt, resolvedAt, etc.). Map the full `BriefItem` into each activity row and reuse `BriefService.enrichBriefsWithAgents` to populate the participant avatars. The CLI and prompt formatter now compose the action + comment display string themselves. * 🐛 fix: degrade gracefully when brief agent enrichment fails getTaskDetail was calling BriefService.enrichBriefsWithAgents inside Promise.all without a fallback, so a failure in the agent-tree lookup would reject the whole request — a regression vs. the existing .catch(() => []) pattern used by other activity reads in this method. Fall back to agentless briefs on error so the task detail keeps rendering. --- apps/cli/src/commands/task/index.ts | 7 +- packages/prompts/src/prompts/task/index.ts | 7 +- packages/types/src/task/index.ts | 18 +++++ src/server/services/task/index.test.ts | 92 +++++++++++++++++++++- src/server/services/task/index.ts | 31 ++++++-- 5 files changed, 143 insertions(+), 12 deletions(-) diff --git a/apps/cli/src/commands/task/index.ts b/apps/cli/src/commands/task/index.ts index 2ffcc850a0..ea8c9d1fee 100644 --- a/apps/cli/src/commands/task/index.ts +++ b/apps/cli/src/commands/task/index.ts @@ -466,7 +466,12 @@ export function registerTaskCommand(program: Command) { : act.priority === 'normal' ? pc.yellow(' [normal]') : ''; - const resolved = act.resolvedAction ? pc.green(` ✏️ ${act.resolvedAction}`) : ''; + const resolvedLabel = act.resolvedAction + ? act.resolvedComment + ? `${act.resolvedAction}: ${act.resolvedComment}` + : act.resolvedAction + : ''; + const resolved = resolvedLabel ? pc.green(` ✏️ ${resolvedLabel}`) : ''; const typeLabel = pc.dim(`[${act.briefType}]`); console.log( ` ${icon} ${pc.dim(ago.padStart(7))} Brief ${typeLabel} ${act.title}${pri}${resolved}${idSuffix}`, diff --git a/packages/prompts/src/prompts/task/index.ts b/packages/prompts/src/prompts/task/index.ts index 927418d2d6..9ca61d13bb 100644 --- a/packages/prompts/src/prompts/task/index.ts +++ b/packages/prompts/src/prompts/task/index.ts @@ -210,7 +210,12 @@ export const formatTaskDetail = (t: TaskDetailData): string => { ` 💬 ${act.time || ''} Topic #${act.seq || '?'} ${act.title || 'Untitled'} ${statusIcon(status)} ${status}${idSuffix}`, ); } else if (act.type === 'brief') { - const resolved = act.resolvedAction ? ` ✏️ ${act.resolvedAction}` : ''; + const resolvedLabel = act.resolvedAction + ? act.resolvedComment + ? `${act.resolvedAction}: ${act.resolvedComment}` + : act.resolvedAction + : ''; + const resolved = resolvedLabel ? ` ✏️ ${resolvedLabel}` : ''; const priStr = act.priority ? ` [${act.priority}]` : ''; lines.push( ` ${briefIcon(act.briefType || '')} ${act.time || ''} Brief [${act.briefType}] ${act.title}${priStr}${resolved}${idSuffix}`, diff --git a/packages/types/src/task/index.ts b/packages/types/src/task/index.ts index 41e5f01db6..d3e18a219b 100644 --- a/packages/types/src/task/index.ts +++ b/packages/types/src/task/index.ts @@ -146,20 +146,38 @@ export interface TaskDetailActivityAuthor { type: 'agent' | 'user'; } +export interface TaskDetailActivityAgent { + avatar: string | null; + backgroundColor: string | null; + id: string; + title: string | null; +} + export interface TaskDetailActivity { + actions?: unknown; agentId?: string | null; + agents?: TaskDetailActivityAgent[]; + artifacts?: unknown; author?: TaskDetailActivityAuthor; briefType?: string; content?: string; + createdAt?: string; + cronJobId?: string | null; id?: string; priority?: string | null; + readAt?: string | null; resolvedAction?: string | null; + resolvedAt?: string | null; + resolvedComment?: string | null; seq?: number | null; status?: string | null; summary?: string; + taskId?: string | null; time?: string; title?: string; + topicId?: string | null; type: 'brief' | 'comment' | 'topic'; + userId?: string | null; } export interface TaskDetailData { diff --git a/src/server/services/task/index.test.ts b/src/server/services/task/index.test.ts index 672ba858d4..b00c02b9a6 100644 --- a/src/server/services/task/index.test.ts +++ b/src/server/services/task/index.test.ts @@ -47,6 +47,7 @@ describe('TaskService', () => { getDependencies: vi.fn(), getDependenciesByTaskIds: vi.fn(), getReviewConfig: vi.fn(), + getTreeAgentIdsForTaskIds: vi.fn().mockResolvedValue({}), getTreePinnedDocuments: vi.fn(), resolve: vi.fn(), }; @@ -874,7 +875,7 @@ describe('TaskService', () => { expect(result?.workspace).toBeUndefined(); }); - it('should build brief activities with resolvedAction concatenated with resolvedComment', async () => { + it('should build brief activities with full BriefItem fields', async () => { const task = { assigneeAgentId: null, assigneeUserId: null, @@ -896,14 +897,23 @@ describe('TaskService', () => { const briefs = [ { + actions: [{ key: 'approve', label: '✅', type: 'resolve' }], + agentId: 'agent-1', + artifacts: ['doc_1'], createdAt: new Date('2024-01-01T00:00:00Z'), + cronJobId: null, id: 'brief-1', priority: 'urgent', + readAt: new Date('2024-01-01T01:00:00Z'), resolvedAction: 'approved', + resolvedAt: new Date('2024-01-01T02:00:00Z'), resolvedComment: 'looks good', summary: 'Review brief', + taskId: 'task_001', title: 'Approval', + topicId: null, type: 'decision', + userId: 'user-1', }, ]; @@ -917,22 +927,38 @@ describe('TaskService', () => { mockTaskModel.findByIds.mockResolvedValue([]); mockTaskModel.getCheckpointConfig.mockReturnValue({}); mockTaskModel.getReviewConfig.mockReturnValue(undefined); + mockTaskModel.getTreeAgentIdsForTaskIds.mockResolvedValue({ task_001: ['agent-1'] }); + mockAgentModel.getAgentAvatarsByIds.mockResolvedValue([ + { avatar: 'avatar.png', backgroundColor: '#fff', id: 'agent-1', title: 'Agent One' }, + ]); const service = new TaskService(db, userId); const result = await service.getTaskDetail('TASK-1'); expect(result?.activities?.[0]).toMatchObject({ + actions: [{ key: 'approve', label: '✅', type: 'resolve' }], + agentId: 'agent-1', + agents: [ + { avatar: 'avatar.png', backgroundColor: '#fff', id: 'agent-1', title: 'Agent One' }, + ], + artifacts: ['doc_1'], briefType: 'decision', + createdAt: '2024-01-01T00:00:00.000Z', id: 'brief-1', priority: 'urgent', - resolvedAction: 'approved: looks good', + readAt: '2024-01-01T01:00:00.000Z', + resolvedAction: 'approved', + resolvedAt: '2024-01-01T02:00:00.000Z', + resolvedComment: 'looks good', summary: 'Review brief', + taskId: 'task_001', title: 'Approval', type: 'brief', + userId: 'user-1', }); }); - it('should use only resolvedAction when resolvedComment is absent', async () => { + it('should keep resolvedAction and resolvedComment as separate fields', async () => { const task = { assigneeAgentId: null, assigneeUserId: null, @@ -981,6 +1007,66 @@ describe('TaskService', () => { expect(result?.activities?.[0]).toMatchObject({ resolvedAction: 'retry', + resolvedComment: null, + type: 'brief', + }); + }); + + it('should still return task detail when brief agent enrichment fails', async () => { + const task = { + assigneeAgentId: null, + assigneeUserId: null, + createdAt: null, + description: null, + error: null, + heartbeatInterval: null, + heartbeatTimeout: null, + id: 'task_001', + identifier: 'TASK-1', + instruction: null, + lastHeartbeatAt: null, + name: 'Task 1', + parentTaskId: null, + priority: 'normal', + status: 'todo', + totalTopics: 0, + }; + + const briefs = [ + { + agentId: 'agent-1', + createdAt: new Date('2024-01-01T00:00:00Z'), + id: 'brief-1', + priority: 'normal', + resolvedAction: null, + resolvedComment: null, + summary: 'Brief', + taskId: 'task_001', + title: 'Brief A', + type: 'insight', + }, + ]; + + mockTaskModel.resolve.mockResolvedValue(task); + mockTaskModel.findAllDescendants.mockResolvedValue([]); + mockTaskModel.getDependencies.mockResolvedValue([]); + mockTaskTopicModel.findWithHandoff.mockResolvedValue([]); + mockBriefModel.findByTaskId.mockResolvedValue(briefs); + mockTaskModel.getComments.mockResolvedValue([]); + mockTaskModel.getTreePinnedDocuments.mockResolvedValue({ nodeMap: {}, tree: [] }); + mockTaskModel.findByIds.mockResolvedValue([]); + mockTaskModel.getCheckpointConfig.mockReturnValue({}); + mockTaskModel.getReviewConfig.mockReturnValue(undefined); + mockTaskModel.getTreeAgentIdsForTaskIds.mockRejectedValue(new Error('DB error')); + + const service = new TaskService(db, userId); + const result = await service.getTaskDetail('TASK-1'); + + expect(result).not.toBeNull(); + expect(result?.activities).toHaveLength(1); + expect(result?.activities?.[0]).toMatchObject({ + agents: [], + id: 'brief-1', type: 'brief', }); }); diff --git a/src/server/services/task/index.ts b/src/server/services/task/index.ts index e3f9d532a5..68f18699d1 100644 --- a/src/server/services/task/index.ts +++ b/src/server/services/task/index.ts @@ -15,11 +15,14 @@ import { TaskTopicModel } from '@/database/models/taskTopic'; import { UserModel } from '@/database/models/user'; import type { LobeChatDatabase } from '@/database/type'; +import { BriefService } from '../brief'; + const emptyWorkspace: WorkspaceData = { nodeMap: {}, tree: [] }; export class TaskService { private agentModel: AgentModel; private briefModel: BriefModel; + private briefService: BriefService; private db: LobeChatDatabase; private taskModel: TaskModel; private taskTopicModel: TaskTopicModel; @@ -30,6 +33,7 @@ export class TaskService { this.taskModel = new TaskModel(db, userId); this.taskTopicModel = new TaskTopicModel(db, userId); this.briefModel = new BriefModel(db, userId); + this.briefService = new BriefService(db, userId); } async getTaskDetail(taskIdOrIdentifier: string): Promise { @@ -135,7 +139,12 @@ export class TaskService { if (c.authorUserId) userIds.add(c.authorUserId); } - const authorMap = await this.resolveAuthors(agentIds, userIds); + const [authorMap, enrichedBriefs] = await Promise.all([ + this.resolveAuthors(agentIds, userIds), + this.briefService + .enrichBriefsWithAgents(briefs) + .catch(() => briefs.map((b) => ({ ...b, agents: [] }))), + ]); const activities: TaskDetailActivity[] = [ ...topics.map((t) => ({ @@ -147,20 +156,28 @@ export class TaskService { title: (t.handoff as TaskTopicHandoff | null)?.title || 'Untitled', type: 'topic' as const, })), - ...briefs.map((b) => ({ + ...enrichedBriefs.map((b) => ({ + actions: b.actions ?? undefined, + agentId: b.agentId, + agents: b.agents, + artifacts: b.artifacts ?? undefined, author: b.agentId ? authorMap.get(b.agentId) : undefined, briefType: b.type, + createdAt: toISO(b.createdAt), + cronJobId: b.cronJobId, id: b.id, priority: b.priority, - resolvedAction: b.resolvedAction - ? b.resolvedComment - ? `${b.resolvedAction}: ${b.resolvedComment}` - : b.resolvedAction - : undefined, + readAt: toISO(b.readAt), + resolvedAction: b.resolvedAction, + resolvedAt: toISO(b.resolvedAt), + resolvedComment: b.resolvedComment, summary: b.summary, + taskId: b.taskId, time: toISO(b.createdAt), title: b.title, + topicId: b.topicId, type: 'brief' as const, + userId: b.userId, })), ...comments.map((c) => ({ agentId: c.authorAgentId,