feat(beaver): 完成Task Team功能v1实现,重构后端架构支持统一内核
新增内部Task系统,包括验证、反馈门控机制,实现自动质量验证 (通过率>=0.75)和用户反馈闭环(satisfied/revise/abandon)。 实现Agent Team v1协调器,支持sequence/parallel/dag执行策略, sub-agent复用主AgentLoop,每个run使用独立memory snapshot。 建立Skill学习pipeline,包含draft/审核/发布/回滚完整生命周期, 通过Task验证通过且用户满意才生成学习候选。 重构目录结构,移除third_party依赖,建立统一engine内核, 所有agent共享运行时基础组件。 更新ContextBuilder清理provider消息字段,增强SkillContext版本管理, 集成TaskExecutionPlanner和TaskSkillResolver实现技能解析机制。
This commit is contained in:
@ -161,6 +161,36 @@ function runSummary(run: ProcessRun, feed: AgentFeedItem[], locale: 'zh-CN' | 'e
|
||||
return latestAssistant?.text || pickAppText(locale, '已完成子任务处理', 'Subtask processing completed');
|
||||
}
|
||||
|
||||
function SkillChips({ metadata }: { metadata?: Record<string, unknown> }) {
|
||||
const rawSelected = metadata?.selected_skill_names;
|
||||
const rawEphemeral = metadata?.ephemeral_skill_names;
|
||||
const selected = Array.isArray(rawSelected) ? rawSelected.map(String).filter(Boolean) : [];
|
||||
const ephemeral = Array.isArray(rawEphemeral) ? rawEphemeral.map(String).filter(Boolean) : [];
|
||||
const draftId = typeof metadata?.generated_skill_draft_id === 'string' ? metadata.generated_skill_draft_id : '';
|
||||
if (selected.length === 0 && ephemeral.length === 0 && !draftId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
||||
{selected.map((name) => (
|
||||
<Badge key={`skill:${name}`} variant="secondary" className="max-w-[128px] truncate text-[10px]">
|
||||
skill:{name}
|
||||
</Badge>
|
||||
))}
|
||||
{ephemeral.map((name) => (
|
||||
<Badge key={`ephemeral:${name}`} variant="outline" className="max-w-[128px] truncate text-[10px]">
|
||||
ephemeral:{name}
|
||||
</Badge>
|
||||
))}
|
||||
{draftId && (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
draft:{draftId.slice(0, 8)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useRunCardPhases(runs: ProcessRun[]) {
|
||||
const [phases, setPhases] = React.useState<Record<string, RunCardPhase>>(() =>
|
||||
Object.fromEntries(
|
||||
@ -288,10 +318,11 @@ function LiveAgentCard({
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
||||
<span className={cn('h-2 w-2 rounded-full', accent.dot)} />
|
||||
<span>{pickAppText(locale, '子 Agent', 'Sub-agent')}</span>
|
||||
<span>{pickAppText(locale, '子任务', 'Subtask')}</span>
|
||||
</div>
|
||||
<div className={cn('mt-1 truncate text-sm font-semibold', accent.title)}>{run.actor_name}</div>
|
||||
<div className="mt-1 line-clamp-2 text-xs text-muted-foreground">{run.title}</div>
|
||||
<SkillChips metadata={run.metadata} />
|
||||
</div>
|
||||
<Badge variant="outline" className={cn('border', statusTone(run.status))}>
|
||||
{appStatusLabel(run.status, locale)}
|
||||
@ -302,7 +333,7 @@ function LiveAgentCard({
|
||||
<div className="max-h-[280px] space-y-2.5 overflow-y-auto pr-1">
|
||||
{feed.length === 0 && (
|
||||
<div className="rounded-2xl border border-dashed border-border/60 bg-background/60 px-4 py-5 text-center text-sm text-muted-foreground">
|
||||
{pickAppText(locale, '等待子 agent 输出...', 'Waiting for sub-agent output...')}
|
||||
{pickAppText(locale, '等待子任务输出...', 'Waiting for subtask output...')}
|
||||
</div>
|
||||
)}
|
||||
{feed.map((item) => (
|
||||
@ -445,13 +476,13 @@ export function AgentTeamBlock({
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '智能体团队', 'Agent team')}
|
||||
{pickAppText(locale, '任务子流程', 'Task subprocess')}
|
||||
</div>
|
||||
<div className="mt-1.5 text-base font-semibold text-foreground">{rootRun.title}</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{liveCount > 0
|
||||
? pickAppText(locale, `主 agent 正在协调 ${liveCount} 个运行中的 sub-agent`, `Lead agent is coordinating ${liveCount} running sub-agents`)
|
||||
: pickAppText(locale, '子 agent 已完成,结果已折叠为摘要卡片', 'Sub-agents are done. Results are folded into summary cards')}
|
||||
? pickAppText(locale, `主 Agent 正在协调 ${liveCount} 个运行中的子任务`, `Main Agent is coordinating ${liveCount} running subtasks`)
|
||||
: pickAppText(locale, '子任务已完成,结果已折叠为摘要卡片', 'Subtasks are done. Results are folded into summary cards')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -462,7 +493,7 @@ export function AgentTeamBlock({
|
||||
</Button>
|
||||
)}
|
||||
<Badge variant="outline" className="border-border/70 bg-background/55 text-foreground/85">
|
||||
{pickAppText(locale, `${memberRuns.length} 个 sub-agent`, `${memberRuns.length} sub-agents`)}
|
||||
{pickAppText(locale, `${memberRuns.length} 个子任务`, `${memberRuns.length} subtasks`)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className={cn('border', statusTone(rootRun.status))}>
|
||||
{appStatusLabel(rootRun.status, locale)}
|
||||
|
||||
@ -6,6 +6,7 @@ import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/t
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { MessageList } from '@/components/chat-workbench/MessageList';
|
||||
import { ArtifactSidebar } from '@/components/chat-workbench/ArtifactSidebar';
|
||||
import { ProcessLane } from '@/components/chat-workbench/ProcessLane';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
|
||||
@ -20,6 +21,7 @@ export function ChatWorkbench({
|
||||
selectedRunId,
|
||||
onSelectRun,
|
||||
onCancelRun,
|
||||
onFeedback,
|
||||
}: {
|
||||
messages: ChatMessage[];
|
||||
isThinking: boolean;
|
||||
@ -31,6 +33,7 @@ export function ChatWorkbench({
|
||||
selectedRunId: string | null;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onCancelRun: (runId: string) => void;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon') => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const [isDesktop, setIsDesktop] = React.useState(() =>
|
||||
@ -72,9 +75,14 @@ export function ChatWorkbench({
|
||||
selectedRunArtifacts.length > 0
|
||||
)
|
||||
);
|
||||
const desktopColumns = hasResultsPanel
|
||||
? 'grid-cols-[minmax(0,1fr)_360px]'
|
||||
: 'grid-cols-[minmax(0,1fr)]';
|
||||
const hasProcessPanel = processRuns.length > 0;
|
||||
const desktopColumns = hasProcessPanel && hasResultsPanel
|
||||
? 'grid-cols-[minmax(0,1fr)_340px_360px]'
|
||||
: hasProcessPanel
|
||||
? 'grid-cols-[minmax(0,1fr)_340px]'
|
||||
: hasResultsPanel
|
||||
? 'grid-cols-[minmax(0,1fr)_360px]'
|
||||
: 'grid-cols-[minmax(0,1fr)]';
|
||||
|
||||
const messageList = (
|
||||
<MessageList
|
||||
@ -88,6 +96,7 @@ export function ChatWorkbench({
|
||||
selectedRunId={selectedRun?.run_id || null}
|
||||
onSelectRun={onSelectRun}
|
||||
onCancelRun={onCancelRun}
|
||||
onFeedback={onFeedback}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -97,6 +106,17 @@ export function ChatWorkbench({
|
||||
<div className="min-h-0">
|
||||
{messageList}
|
||||
</div>
|
||||
{hasProcessPanel && (
|
||||
<div className="min-h-0">
|
||||
<ProcessLane
|
||||
runs={processRuns}
|
||||
events={processEvents}
|
||||
selectedRunId={selectedRun?.run_id || null}
|
||||
onSelectRun={onSelectRun}
|
||||
onCancelRun={onCancelRun}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasResultsPanel && (
|
||||
<div className="min-h-0">
|
||||
<ArtifactSidebar
|
||||
@ -112,26 +132,40 @@ export function ChatWorkbench({
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
{!hasResultsPanel ? (
|
||||
{!hasResultsPanel && !hasProcessPanel ? (
|
||||
messageList
|
||||
) : (
|
||||
<Tabs defaultValue="chat" className="h-full flex flex-col">
|
||||
<div className="px-4 pt-3 border-b border-border">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsList className={`grid w-full ${hasResultsPanel ? 'grid-cols-3' : 'grid-cols-2'}`}>
|
||||
<TabsTrigger value="chat">{pickAppText(locale, '聊天', 'Chat')}</TabsTrigger>
|
||||
<TabsTrigger value="results">{pickAppText(locale, '结果', 'Results')}</TabsTrigger>
|
||||
<TabsTrigger value="process">{pickAppText(locale, '过程', 'Process')}</TabsTrigger>
|
||||
{hasResultsPanel && (
|
||||
<TabsTrigger value="results">{pickAppText(locale, '结果', 'Results')}</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="chat" className="flex-1 min-h-0 mt-0">
|
||||
{messageList}
|
||||
</TabsContent>
|
||||
<TabsContent value="results" className="flex-1 min-h-0 mt-0">
|
||||
<ArtifactSidebar
|
||||
selectedRun={selectedRun}
|
||||
<TabsContent value="process" className="flex-1 min-h-0 mt-0">
|
||||
<ProcessLane
|
||||
runs={processRuns}
|
||||
events={processEvents}
|
||||
artifacts={processArtifacts}
|
||||
selectedRunId={selectedRun?.run_id || null}
|
||||
onSelectRun={onSelectRun}
|
||||
onCancelRun={onCancelRun}
|
||||
/>
|
||||
</TabsContent>
|
||||
{hasResultsPanel && (
|
||||
<TabsContent value="results" className="flex-1 min-h-0 mt-0">
|
||||
<ArtifactSidebar
|
||||
selectedRun={selectedRun}
|
||||
events={processEvents}
|
||||
artifacts={processArtifacts}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Bot, Loader2, Paperclip, User } from 'lucide-react';
|
||||
import { Bot, Loader2, Paperclip, RefreshCcw, ThumbsUp, User, XCircle } from 'lucide-react';
|
||||
|
||||
import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
|
||||
import { getAccessToken, getFileUrl } from '@/lib/api';
|
||||
@ -37,7 +37,16 @@ function AuthImage({ src, alt, className }: { src: string; alt: string; classNam
|
||||
return <img src={blobUrl} alt={alt} className={className} loading="lazy" decoding="async" />;
|
||||
}
|
||||
|
||||
function MessageBubble({ message }: { message: ChatMessage }) {
|
||||
function MessageBubble({
|
||||
message,
|
||||
canSendFeedback,
|
||||
onFeedback,
|
||||
}: {
|
||||
message: ChatMessage;
|
||||
canSendFeedback: boolean;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon') => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const isUser = message.role === 'user';
|
||||
const textContent = typeof message.content === 'string' ? message.content : String(message.content || '');
|
||||
|
||||
@ -101,6 +110,56 @@ function MessageBubble({ message }: { message: ChatMessage }) {
|
||||
) : (
|
||||
<MarkdownContent content={textContent} />
|
||||
)}
|
||||
{!isUser && canSendFeedback && message.run_id && (
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 border-t border-border/70 pt-2">
|
||||
{message.feedback_state ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{message.feedback_state === 'satisfied'
|
||||
? pickAppText(locale, '已标记满意', 'Marked satisfied')
|
||||
: message.feedback_state === 'revise'
|
||||
? pickAppText(locale, '已请求修改', 'Revision requested')
|
||||
: pickAppText(locale, '已放弃任务', 'Task abandoned')}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFeedback(message.run_id!, 'satisfied')}
|
||||
className="inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<ThumbsUp className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '满意', 'Satisfied')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFeedback(message.run_id!, 'revise')}
|
||||
className="inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<RefreshCcw className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '需要修改', 'Revise')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFeedback(message.run_id!, 'abandon')}
|
||||
className="inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '放弃', 'Abandon')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{message.validation_status && message.validation_status !== 'unknown' && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{message.validation_status === 'passed'
|
||||
? pickAppText(locale, '验证通过', 'Validated')
|
||||
: pickAppText(locale, '验证未通过', 'Validation failed')}
|
||||
</span>
|
||||
)}
|
||||
{message.feedback_error && (
|
||||
<span className="text-xs text-destructive">{message.feedback_error}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isUser && (
|
||||
<div className="w-7 h-7 rounded-full bg-secondary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
@ -198,6 +257,7 @@ export function MessageList({
|
||||
selectedRunId,
|
||||
onSelectRun,
|
||||
onCancelRun,
|
||||
onFeedback,
|
||||
}: {
|
||||
messages: ChatMessage[];
|
||||
isThinking: boolean;
|
||||
@ -209,6 +269,7 @@ export function MessageList({
|
||||
selectedRunId: string | null;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onCancelRun: (runId: string) => void;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon') => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const visibleMessages = React.useMemo(
|
||||
@ -245,6 +306,9 @@ export function MessageList({
|
||||
return a.order - b.order;
|
||||
});
|
||||
}, [teamGroups, visibleMessages]);
|
||||
const latestAssistantRunId = [...visibleMessages]
|
||||
.reverse()
|
||||
.find((message) => message.role === 'assistant' && message.run_id && message.task_id)?.run_id;
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full px-4" viewportRef={viewportRef}>
|
||||
@ -259,7 +323,12 @@ export function MessageList({
|
||||
|
||||
{timelineItems.map((item) =>
|
||||
item.kind === 'message' ? (
|
||||
<MessageBubble key={item.key} message={item.message} />
|
||||
<MessageBubble
|
||||
key={item.key}
|
||||
message={item.message}
|
||||
canSendFeedback={Boolean(latestAssistantRunId && item.message.run_id === latestAssistantRunId)}
|
||||
onFeedback={onFeedback}
|
||||
/>
|
||||
) : (
|
||||
<AgentTeamBlock
|
||||
key={item.key}
|
||||
|
||||
@ -127,6 +127,7 @@ export function ProcessLane({
|
||||
{run.summary}
|
||||
</div>
|
||||
)}
|
||||
<SkillMetadata metadata={run.metadata} />
|
||||
<div className="space-y-1.5">
|
||||
{runEvents.length === 0 && run.status === 'running' && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
@ -161,3 +162,33 @@ export function ProcessLane({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillMetadata({ metadata }: { metadata?: Record<string, unknown> }) {
|
||||
const rawSelected = metadata?.selected_skill_names;
|
||||
const rawEphemeral = metadata?.ephemeral_skill_names;
|
||||
const selected = Array.isArray(rawSelected) ? rawSelected.map(String).filter(Boolean) : [];
|
||||
const ephemeral = Array.isArray(rawEphemeral) ? rawEphemeral.map(String).filter(Boolean) : [];
|
||||
const draftId = typeof metadata?.generated_skill_draft_id === 'string' ? metadata.generated_skill_draft_id : '';
|
||||
if (selected.length === 0 && ephemeral.length === 0 && !draftId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5 text-[11px]">
|
||||
{selected.map((name) => (
|
||||
<Badge key={`skill:${name}`} variant="secondary" className="text-[10px]">
|
||||
skill:{name}
|
||||
</Badge>
|
||||
))}
|
||||
{ephemeral.map((name) => (
|
||||
<Badge key={`ephemeral:${name}`} variant="outline" className="text-[10px]">
|
||||
ephemeral:{name}
|
||||
</Badge>
|
||||
))}
|
||||
{draftId && (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
draft:{draftId.slice(0, 8)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user