添加 DEFAULT_TEAM_NODE_MAX_TOOL_ITERATIONS 配置项以控制团队节点的最大工具迭代次数, 并修改 LocalAgentRunner 中的逻辑来使用此默认值当 envelope 中未指定时。 fix(runtime): 修复团队节点运行成功判断逻辑 更新运行成功判断条件,将 finish_reason 为 "max_tool_iterations_finalized" 的情况 视为运行失败,并添加对原始工具调用输出的检测,避免将其误判为成功完成。 feat(mcp): 添加团队工作流MCP工具类别支持 增加新的本地MCP工具类别 "team_workflow" 及其对应的工具创建功能, 为团队工作流提供本地工具支持。 refactor(engine): 调整AgentLoop最大工具迭代次数设置 将 AgentProfile 中的默认 max_tool_iterations 从 30 增加到 100, 同时移除 TaskExecutionPlanner 构造函数中的重复参数传递。 perf(mcp): 优化MCP连接管理避免重复连接 添加 mcp_connected 标志来跟踪MCP连接状态,确保 connect_all 只执行一次, 提高性能并避免不必要的重复连接。 refactor(skills): 移除技能团队模板相关功能 移除与技能团队模板相关的代码,包括解析、存储和处理逻辑, 简化技能记录结构和加载流程。 feat(process): 增强会话过程投影器功能 添加技能激活快照事件处理,改进团队运行完成消息显示, 并增强技能激活事件的时间戳记录功能。 refactor(tasks): 简化任务尝试编排器团队执行逻辑 移除团队执行相关代码,将所有任务统一按单步执行处理, 简化任务编排器的复杂度并提升执行效率。 fix(evidence): 修复节点证据评估中需求验证逻辑 更新节点证据评估逻辑,跳过自然语言证据需求的确定性验证, 只执行机器可读的需求验证,避免因自然语言需求导致的节点失败。
693 lines
23 KiB
TypeScript
693 lines
23 KiB
TypeScript
import { pickAppText, type AppLocale } from '@/lib/i18n/core';
|
|
import type { BackendTask, ProcessArtifact, ProcessRun, SessionProcessProjection, TaskTimelineCard } from '@/types';
|
|
|
|
export type TaskUiStatus = 'done' | 'running' | 'waiting' | 'error' | 'cancelled';
|
|
|
|
export type TaskUiStep = {
|
|
id: string;
|
|
title: string;
|
|
summary: string;
|
|
status: TaskUiStatus;
|
|
createdAt: string;
|
|
kind: 'task' | 'skill' | 'tool' | 'agent' | 'artifact' | 'result';
|
|
};
|
|
|
|
export type TaskUiSkill = {
|
|
id: string;
|
|
name: string;
|
|
summary: string;
|
|
status: TaskUiStatus;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type TaskUiToolCall = {
|
|
id: string;
|
|
runId?: string | null;
|
|
toolCallId?: string | null;
|
|
toolName: string;
|
|
actorName: string;
|
|
summary: string;
|
|
status: TaskUiStatus;
|
|
createdAt: string;
|
|
finishedAt?: string;
|
|
durationMs?: number | null;
|
|
query?: string;
|
|
url?: string;
|
|
quality?: string;
|
|
resultCount?: number;
|
|
};
|
|
|
|
export type TaskUiAttemptRun = {
|
|
runId: string;
|
|
title: string;
|
|
actorName: string;
|
|
source?: string | null;
|
|
status: TaskUiStatus;
|
|
startedAt: string;
|
|
finishedAt?: string | null;
|
|
};
|
|
|
|
export type TaskUiAttempt = {
|
|
id: string;
|
|
index: number;
|
|
title: string;
|
|
status: TaskUiStatus;
|
|
startedAt: string;
|
|
finishedAt?: string | null;
|
|
runs: TaskUiAttemptRun[];
|
|
tools: TaskUiToolCall[];
|
|
result?: {
|
|
title: string;
|
|
summary: string;
|
|
status: TaskUiStatus;
|
|
createdAt: string;
|
|
};
|
|
};
|
|
|
|
export type TaskUiAgentNode = {
|
|
runId: string;
|
|
parentRunId: string | null;
|
|
name: string;
|
|
title: string;
|
|
summary: string;
|
|
status: TaskUiStatus;
|
|
progress: number;
|
|
children: TaskUiAgentNode[];
|
|
};
|
|
|
|
export type TaskUiArtifact = {
|
|
id: string;
|
|
runId: string;
|
|
actorName?: string;
|
|
title: string;
|
|
type: ProcessArtifact['artifact_type'];
|
|
summary: string;
|
|
createdAt: string;
|
|
fileId?: string;
|
|
url?: string;
|
|
status: TaskUiStatus;
|
|
sizeLabel?: string;
|
|
};
|
|
|
|
export type TaskUiModel = {
|
|
executionMode: 'single' | 'team';
|
|
team: {
|
|
hasTeam: boolean;
|
|
status: TaskUiStatus;
|
|
outcome: string;
|
|
nodeIds: string[];
|
|
incompleteNodeIds: string[];
|
|
summary: string;
|
|
};
|
|
summary: TaskUiStep;
|
|
skills: TaskUiSkill[];
|
|
tools: TaskUiToolCall[];
|
|
attempts: TaskUiAttempt[];
|
|
agentTree: TaskUiAgentNode[];
|
|
artifacts: TaskUiArtifact[];
|
|
steps: TaskUiStep[];
|
|
result: {
|
|
status: TaskUiStatus;
|
|
title: string;
|
|
summary: string;
|
|
bullets: string[];
|
|
};
|
|
};
|
|
|
|
const WAITING_TASK_STATUSES = new Set(['open', 'queued', 'awaiting_acceptance', 'needs_revision']);
|
|
const RUNNING_TASK_STATUSES = new Set(['running']);
|
|
const DONE_TASK_STATUSES = new Set(['closed', 'done', 'completed', 'satisfied']);
|
|
const ERROR_TASK_STATUSES = new Set(['error', 'failed']);
|
|
const CANCELLED_TASK_STATUSES = new Set(['cancelled', 'abandoned']);
|
|
|
|
function normalizeStatus(status?: string | null): TaskUiStatus {
|
|
const value = String(status || '').toLowerCase();
|
|
if (DONE_TASK_STATUSES.has(value) || value === 'done') return 'done';
|
|
if (RUNNING_TASK_STATUSES.has(value)) return 'running';
|
|
if (ERROR_TASK_STATUSES.has(value)) return 'error';
|
|
if (CANCELLED_TASK_STATUSES.has(value)) return 'cancelled';
|
|
if (WAITING_TASK_STATUSES.has(value) || value === 'waiting' || value === 'queued' || !value) return 'waiting';
|
|
return 'running';
|
|
}
|
|
|
|
export function taskUiStatusLabel(status: TaskUiStatus, locale: AppLocale | string): string {
|
|
const labels: Record<TaskUiStatus, [string, string]> = {
|
|
done: ['已完成', 'Done'],
|
|
running: ['进行中', 'Running'],
|
|
waiting: ['等待中', 'Waiting'],
|
|
error: ['失败', 'Failed'],
|
|
cancelled: ['已取消', 'Cancelled'],
|
|
};
|
|
const label = labels[status];
|
|
return pickAppText(locale, label[0], label[1]);
|
|
}
|
|
|
|
export function taskUiStatusClass(status: TaskUiStatus): string {
|
|
if (status === 'done') return 'border-[#D6E2D5] bg-[#F4F8F3] text-[#557052]';
|
|
if (status === 'running') return 'border-[#E8D7B2] bg-[#FFF8EA] text-[#9B6B12]';
|
|
if (status === 'error') return 'border-[#E8C8C2] bg-[#FFF4F2] text-[#9D3D2F]';
|
|
if (status === 'cancelled') return 'border-[#DED9D5] bg-[#F4F2F0] text-[#756A64]';
|
|
return 'border-[#DDD8D4] bg-[#F8F6F4] text-[#746C67]';
|
|
}
|
|
|
|
function titleForTask(task: BackendTask): string {
|
|
return task.short_title || String(task.metadata?.short_title || '') || task.description || task.goal || task.task_id;
|
|
}
|
|
|
|
function summarizeTask(task: BackendTask): string {
|
|
return task.description || task.goal || String(task.metadata?.summary || '') || task.task_id;
|
|
}
|
|
|
|
function statusRank(status: TaskUiStatus): number {
|
|
if (status === 'error') return 5;
|
|
if (status === 'running') return 4;
|
|
if (status === 'waiting') return 3;
|
|
if (status === 'cancelled') return 2;
|
|
return 1;
|
|
}
|
|
|
|
function mergeStatus(current: TaskUiStatus, next: TaskUiStatus): TaskUiStatus {
|
|
return statusRank(next) > statusRank(current) ? next : current;
|
|
}
|
|
|
|
function mergeToolStatus(current: TaskUiStatus, next: TaskUiStatus): TaskUiStatus {
|
|
if (next === 'error' || current === 'error') return 'error';
|
|
if (next === 'done' || current === 'done') return 'done';
|
|
if (next === 'running' || current === 'running') return 'running';
|
|
if (next === 'cancelled' || current === 'cancelled') return 'cancelled';
|
|
return 'waiting';
|
|
}
|
|
|
|
function firstString(...values: unknown[]): string {
|
|
for (const value of values) {
|
|
if (typeof value === 'string' && value.trim()) {
|
|
return value.trim();
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function stringList(value: unknown): string[] {
|
|
if (!Array.isArray(value)) return [];
|
|
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
|
}
|
|
|
|
function parseJsonObject(value: unknown): Record<string, unknown> | null {
|
|
if (!value || typeof value !== 'string') return null;
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function countArray(value: unknown): number | undefined {
|
|
return Array.isArray(value) ? value.length : undefined;
|
|
}
|
|
|
|
function summarizeToolPayload(toolName: string, card: TaskTimelineCard): {
|
|
summary: string;
|
|
query?: string;
|
|
url?: string;
|
|
quality?: string;
|
|
resultCount?: number;
|
|
} {
|
|
const details = card.details ?? {};
|
|
const args = parseJsonObject(firstString(details.arguments));
|
|
const result = parseJsonObject(firstString(details.result_summary, card.summary));
|
|
const source = result ?? args ?? {};
|
|
const query = firstString(source.query, args?.query);
|
|
const url = firstString(source.url, args?.url);
|
|
const quality = firstString(source.quality);
|
|
const resultCount = countArray(source.results) ?? countArray(source.links);
|
|
|
|
if (toolName === 'web_search') {
|
|
return {
|
|
query,
|
|
quality,
|
|
resultCount,
|
|
summary: [
|
|
query ? `Query: ${query}` : 'Search query',
|
|
typeof resultCount === 'number' ? `${resultCount} result(s)` : '',
|
|
].filter(Boolean).join(' · '),
|
|
};
|
|
}
|
|
|
|
if (toolName === 'web_fetch') {
|
|
return {
|
|
url,
|
|
resultCount,
|
|
summary: [
|
|
firstString(source.title) || (url ? `Fetch ${url}` : 'Fetch web page'),
|
|
firstString(source.status_code) ? `HTTP ${String(source.status_code)}` : '',
|
|
typeof resultCount === 'number' ? `${resultCount} link(s)` : '',
|
|
].filter(Boolean).join(' · '),
|
|
};
|
|
}
|
|
|
|
return {
|
|
query,
|
|
url,
|
|
quality,
|
|
resultCount,
|
|
summary: firstString(card.summary, details.result_summary, card.title) || toolName,
|
|
};
|
|
}
|
|
|
|
function toolNameFromCard(card: TaskTimelineCard): string {
|
|
return firstString(
|
|
card.details?.tool_name,
|
|
card.details?.tool,
|
|
card.details?.name,
|
|
card.actorName,
|
|
card.title
|
|
);
|
|
}
|
|
|
|
function buildSkills(task: BackendTask, cards: TaskTimelineCard[], locale: AppLocale | string): TaskUiSkill[] {
|
|
const createdAt = task.created_at || task.updated_at;
|
|
const names = new Set(task.skill_names.filter(Boolean));
|
|
for (const card of cards) {
|
|
if (card.type !== 'skill') continue;
|
|
const detailNames = card.details?.skill_names;
|
|
if (Array.isArray(detailNames)) {
|
|
detailNames.forEach((item) => {
|
|
if (typeof item === 'string' && item.trim()) names.add(item.trim());
|
|
});
|
|
}
|
|
}
|
|
|
|
return Array.from(names).map((name, index) => {
|
|
const card = cards.find((item) => item.type === 'skill' && (item.title === name || item.summary?.includes(name)));
|
|
return {
|
|
id: `${name}:${index}`,
|
|
name,
|
|
summary: card?.summary || '',
|
|
status: normalizeStatus(card?.status || (task.status === 'running' || task.status === 'open' ? 'running' : 'done')),
|
|
createdAt: card?.createdAt || createdAt,
|
|
};
|
|
});
|
|
}
|
|
|
|
function toolAggregateStatus(toolCards: TaskTimelineCard[]): TaskUiStatus {
|
|
const calls = new Map<string, { started: boolean; finished: boolean; error: boolean }>();
|
|
for (const card of toolCards) {
|
|
const toolName = toolNameFromCard(card) || card.title;
|
|
const toolCallId = firstString(card.details?.tool_call_id);
|
|
const key = toolCallId ? `${card.runId || '-'}:${toolCallId}` : `${card.runId || '-'}:${toolName}:${card.id}`;
|
|
const item = calls.get(key) ?? { started: false, finished: false, error: false };
|
|
if (card.type === 'tool_call') item.started = true;
|
|
if (card.type === 'tool_result') item.finished = true;
|
|
if (normalizeStatus(card.status) === 'error') item.error = true;
|
|
calls.set(key, item);
|
|
}
|
|
const values = Array.from(calls.values());
|
|
if (values.some((item) => item.error)) return 'error';
|
|
if (values.length > 0 && values.every((item) => item.finished)) return 'done';
|
|
if (values.some((item) => item.started)) return 'running';
|
|
return 'waiting';
|
|
}
|
|
|
|
function toolActorNameForRun(run: ProcessRun | undefined): string {
|
|
if (!run) return 'Agent';
|
|
if (run.source === 'task_team' || run.metadata?.node_id) {
|
|
return run.actor_name || run.title || String(run.metadata?.node_id || 'Agent');
|
|
}
|
|
return 'Agent';
|
|
}
|
|
|
|
function buildTools(cards: TaskTimelineCard[], runs: ProcessRun[]): TaskUiToolCall[] {
|
|
const map = new Map<string, TaskUiToolCall>();
|
|
const runsById = new Map(runs.map((run) => [run.run_id, run]));
|
|
|
|
for (const card of cards) {
|
|
if (card.type !== 'tool_call' && card.type !== 'tool_result') continue;
|
|
const toolName = toolNameFromCard(card) || card.title;
|
|
const toolCallId = firstString(card.details?.tool_call_id);
|
|
const key = toolCallId ? `${card.runId || '-'}:${toolCallId}` : card.id;
|
|
const status = normalizeStatus(card.status || (card.type === 'tool_result' ? 'done' : 'running'));
|
|
const summary = summarizeToolPayload(toolName, card);
|
|
const existing = map.get(key);
|
|
if (existing) {
|
|
const finishedAt = card.type === 'tool_result' ? card.createdAt : existing.finishedAt;
|
|
map.set(key, {
|
|
...existing,
|
|
summary: summary.summary || existing.summary,
|
|
status: mergeToolStatus(existing.status, status),
|
|
finishedAt,
|
|
durationMs: calculateDurationMs(existing.createdAt, finishedAt),
|
|
query: summary.query || existing.query,
|
|
url: summary.url || existing.url,
|
|
quality: summary.quality || existing.quality,
|
|
resultCount: summary.resultCount ?? existing.resultCount,
|
|
});
|
|
continue;
|
|
}
|
|
map.set(key, {
|
|
id: key,
|
|
runId: card.runId,
|
|
toolCallId,
|
|
toolName,
|
|
actorName: toolActorNameForRun(card.runId ? runsById.get(card.runId) : undefined),
|
|
summary: summary.summary || card.title,
|
|
status,
|
|
createdAt: card.createdAt,
|
|
finishedAt: card.type === 'tool_result' ? card.createdAt : undefined,
|
|
durationMs: calculateDurationMs(card.createdAt, card.type === 'tool_result' ? card.createdAt : undefined),
|
|
query: summary.query,
|
|
url: summary.url,
|
|
quality: summary.quality,
|
|
resultCount: summary.resultCount,
|
|
});
|
|
}
|
|
|
|
return Array.from(map.values()).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
}
|
|
|
|
function calculateDurationMs(start?: string | null, end?: string | null): number | null {
|
|
if (!start || !end) return null;
|
|
const startMs = new Date(start).getTime();
|
|
const endMs = new Date(end).getTime();
|
|
if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs < startMs) return null;
|
|
return endMs - startMs;
|
|
}
|
|
|
|
function attemptIndexForRun(run: ProcessRun, fallback: number): number {
|
|
const metadataIndex = run.metadata?.attempt_index;
|
|
if (typeof metadataIndex === 'number' && Number.isFinite(metadataIndex)) return metadataIndex;
|
|
if (typeof metadataIndex === 'string' && /^\d+$/.test(metadataIndex)) return Number(metadataIndex);
|
|
const match = run.run_id.match(/:attempt:(\d+)/);
|
|
return match ? Number(match[1]) : fallback;
|
|
}
|
|
|
|
function runStartedAt(run: ProcessRun): string {
|
|
return run.started_at || run.finished_at || '';
|
|
}
|
|
|
|
function runFinishedAt(run: ProcessRun): string | null | undefined {
|
|
return run.finished_at;
|
|
}
|
|
|
|
function maxTime(values: Array<string | null | undefined>): string | null {
|
|
let selected: string | null = null;
|
|
let selectedMs = -Infinity;
|
|
for (const value of values) {
|
|
if (!value) continue;
|
|
const time = new Date(value).getTime();
|
|
if (!Number.isNaN(time) && time > selectedMs) {
|
|
selected = value;
|
|
selectedMs = time;
|
|
}
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
function attemptStatus(runs: ProcessRun[], planner?: ProcessRun): TaskUiStatus {
|
|
const statuses = [...runs.map((run) => normalizeStatus(run.status)), normalizeStatus(planner?.status)];
|
|
if (statuses.some((status) => status === 'running')) return 'running';
|
|
if (statuses.some((status) => status === 'error')) return 'error';
|
|
if (statuses.some((status) => status === 'cancelled')) return 'cancelled';
|
|
if (statuses.some((status) => status === 'done')) return 'done';
|
|
return 'waiting';
|
|
}
|
|
|
|
function collectChildRunIds(rootRunId: string, runs: ProcessRun[]): Set<string> {
|
|
const ids = new Set([rootRunId]);
|
|
let changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const run of runs) {
|
|
if (run.parent_run_id && ids.has(run.parent_run_id) && !ids.has(run.run_id)) {
|
|
ids.add(run.run_id);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
function buildAttempts(
|
|
process: SessionProcessProjection,
|
|
cards: TaskTimelineCard[],
|
|
tools: TaskUiToolCall[],
|
|
locale: AppLocale | string,
|
|
): TaskUiAttempt[] {
|
|
const runs = process.runs ?? [];
|
|
const plannerRuns = runs
|
|
.filter((run) => run.source === 'task_mode' || /:attempt:\d+/.test(run.run_id))
|
|
.sort((a, b) => new Date(runStartedAt(a)).getTime() - new Date(runStartedAt(b)).getTime());
|
|
|
|
const attemptRuns = plannerRuns.length > 0 ? plannerRuns : [];
|
|
const attempts = attemptRuns.map((planner, index) => {
|
|
const runIds = collectChildRunIds(planner.run_id, runs);
|
|
const groupedRuns = runs
|
|
.filter((run) => runIds.has(run.run_id))
|
|
.sort((a, b) => new Date(runStartedAt(a)).getTime() - new Date(runStartedAt(b)).getTime());
|
|
const groupedTools = tools.filter((tool) => tool.runId && runIds.has(tool.runId));
|
|
const resultCard = [...cards].reverse().find((card) => card.type === 'result' && card.runId && runIds.has(card.runId));
|
|
const attemptIndex = attemptIndexForRun(planner, index + 1);
|
|
return {
|
|
id: planner.run_id,
|
|
index: attemptIndex,
|
|
title: pickAppText(locale, `第 ${attemptIndex} 次执行`, `Attempt ${attemptIndex}`),
|
|
status: attemptStatus(groupedRuns, planner),
|
|
startedAt: runStartedAt(planner),
|
|
finishedAt: maxTime(groupedRuns.map(runFinishedAt)) ?? planner.finished_at,
|
|
runs: groupedRuns.map((run) => ({
|
|
runId: run.run_id,
|
|
title: run.title || run.actor_name || run.actor_id,
|
|
actorName: run.actor_name || run.actor_id,
|
|
source: run.source,
|
|
status: normalizeStatus(run.status),
|
|
startedAt: runStartedAt(run),
|
|
finishedAt: run.finished_at,
|
|
})),
|
|
tools: groupedTools,
|
|
result: resultCard
|
|
? {
|
|
title: resultCard.title,
|
|
summary: resultCard.summary || '',
|
|
status: normalizeStatus(resultCard.status),
|
|
createdAt: resultCard.createdAt,
|
|
}
|
|
: undefined,
|
|
};
|
|
});
|
|
|
|
if (attempts.length > 0) return attempts;
|
|
|
|
return [
|
|
{
|
|
id: 'single-run',
|
|
index: 1,
|
|
title: pickAppText(locale, '本次执行', 'Current run'),
|
|
status: tools.some((tool) => tool.status === 'running') ? 'running' : tools.length > 0 ? 'done' : 'waiting',
|
|
startedAt: cards[0]?.createdAt || '',
|
|
runs: runs.map((run) => ({
|
|
runId: run.run_id,
|
|
title: run.title || run.actor_name || run.actor_id,
|
|
actorName: run.actor_name || run.actor_id,
|
|
source: run.source,
|
|
status: normalizeStatus(run.status),
|
|
startedAt: runStartedAt(run),
|
|
finishedAt: run.finished_at,
|
|
})),
|
|
tools,
|
|
result: undefined,
|
|
},
|
|
];
|
|
}
|
|
|
|
function progressForStatus(status: TaskUiStatus): number {
|
|
if (status === 'done') return 100;
|
|
if (status === 'running') return 58;
|
|
if (status === 'error' || status === 'cancelled') return 100;
|
|
return 12;
|
|
}
|
|
|
|
function buildAgentTree(runs: ProcessRun[]): TaskUiAgentNode[] {
|
|
const nodes = new Map<string, TaskUiAgentNode>();
|
|
const teamRuns = runs.filter((run) => run.source === 'task_team' || Boolean(run.metadata?.node_id));
|
|
for (const run of teamRuns) {
|
|
const status = normalizeStatus(run.status);
|
|
nodes.set(run.run_id, {
|
|
runId: run.run_id,
|
|
parentRunId: run.parent_run_id ?? null,
|
|
name: run.actor_name || run.actor_id || run.title,
|
|
title: run.title || run.actor_name || run.actor_id,
|
|
summary: run.summary || String(run.metadata?.summary || ''),
|
|
status,
|
|
progress: progressForStatus(status),
|
|
children: [],
|
|
});
|
|
}
|
|
|
|
const roots: TaskUiAgentNode[] = [];
|
|
for (const node of Array.from(nodes.values())) {
|
|
if (node.parentRunId && nodes.has(node.parentRunId)) {
|
|
nodes.get(node.parentRunId)!.children.push(node);
|
|
} else {
|
|
roots.push(node);
|
|
}
|
|
}
|
|
|
|
return roots;
|
|
}
|
|
|
|
function buildTeam(cards: TaskTimelineCard[]): TaskUiModel['team'] {
|
|
const teamCard = [...cards].reverse().find((card) => card.type === 'agent_team');
|
|
if (!teamCard) {
|
|
return {
|
|
hasTeam: false,
|
|
status: 'waiting',
|
|
outcome: 'single',
|
|
nodeIds: [],
|
|
incompleteNodeIds: [],
|
|
summary: '',
|
|
};
|
|
}
|
|
const nodeIds = stringList(teamCard.details?.node_ids);
|
|
const incompleteNodeIds = stringList(teamCard.details?.incomplete_node_ids);
|
|
const outcome = firstString(teamCard.details?.task_outcome) || (teamCard.status === 'error' ? 'incomplete' : 'complete');
|
|
return {
|
|
hasTeam: true,
|
|
status: normalizeStatus(teamCard.status),
|
|
outcome,
|
|
nodeIds,
|
|
incompleteNodeIds,
|
|
summary: teamCard.summary || (outcome === 'complete' ? 'Agent Team completed' : 'Team 执行未完成 / 子节点失败'),
|
|
};
|
|
}
|
|
|
|
function buildArtifacts(process: SessionProcessProjection): TaskUiArtifact[] {
|
|
return process.artifacts.map((artifact) => ({
|
|
id: artifact.artifact_id,
|
|
runId: artifact.run_id,
|
|
actorName: artifact.actor_name,
|
|
title: artifact.title || artifact.artifact_id,
|
|
type: artifact.artifact_type,
|
|
summary: firstString(artifact.metadata?.summary, artifact.content, artifact.url) || artifact.artifact_type,
|
|
createdAt: artifact.created_at,
|
|
fileId: artifact.file_id,
|
|
url: artifact.url,
|
|
status: normalizeStatus(firstString(artifact.metadata?.status, artifact.metadata?.state) || 'done'),
|
|
sizeLabel: firstString(artifact.metadata?.size_label, artifact.metadata?.size, artifact.metadata?.file_size),
|
|
}));
|
|
}
|
|
|
|
function stepKind(card: TaskTimelineCard): TaskUiStep['kind'] {
|
|
if (card.type === 'skill') return 'skill';
|
|
if (card.type === 'tool_call' || card.type === 'tool_result') return 'tool';
|
|
if (card.type === 'agent_team' || card.type === 'agent_progress' || card.type === 'agent_handoff') return 'agent';
|
|
if (card.type === 'artifact') return 'artifact';
|
|
if (card.type === 'result' || card.type === 'acceptance') return 'result';
|
|
return 'task';
|
|
}
|
|
|
|
function buildSteps(task: BackendTask, cards: TaskTimelineCard[], locale: AppLocale | string): TaskUiStep[] {
|
|
const taskStep: TaskUiStep = {
|
|
id: `summary:${task.task_id}`,
|
|
title: titleForTask(task),
|
|
summary: summarizeTask(task),
|
|
status: normalizeStatus(task.status),
|
|
createdAt: task.created_at,
|
|
kind: 'task',
|
|
};
|
|
|
|
const skillCard = cards.find((card) => card.type === 'skill');
|
|
const teamCard = cards.find((card) => card.type === 'agent_team');
|
|
const resultCard = [...cards].reverse().find((card) => card.type === 'result');
|
|
const toolCards = cards.filter((card) => card.type === 'tool_call' || card.type === 'tool_result');
|
|
const toolNames = new Set(toolCards.map(toolNameFromCard).filter(Boolean));
|
|
const cardSteps: Array<TaskUiStep | null> = [
|
|
skillCard
|
|
? {
|
|
id: `${skillCard.id}:step`,
|
|
title: pickAppText(locale, '选择 Skill', 'Skill selected'),
|
|
summary: skillCard.summary || '',
|
|
status: normalizeStatus(skillCard.status),
|
|
createdAt: skillCard.createdAt,
|
|
kind: 'skill' as const,
|
|
}
|
|
: null,
|
|
toolCards.length
|
|
? {
|
|
id: `tools:${task.task_id}`,
|
|
title: pickAppText(locale, '调用工具', 'Tool calls'),
|
|
summary: `${toolCards.filter((card) => card.type === 'tool_result').length || toolCards.length} calls · ${Array.from(toolNames).slice(0, 3).join(', ')}`,
|
|
status: toolAggregateStatus(toolCards),
|
|
createdAt: toolCards[0].createdAt,
|
|
kind: 'tool' as const,
|
|
}
|
|
: null,
|
|
teamCard
|
|
? {
|
|
id: `${teamCard.id}:step`,
|
|
title: pickAppText(locale, 'Agent Team 执行', 'Agent Team execution'),
|
|
summary: teamCard.summary || '',
|
|
status: normalizeStatus(teamCard.status),
|
|
createdAt: teamCard.createdAt,
|
|
kind: 'agent' as const,
|
|
}
|
|
: null,
|
|
resultCard
|
|
? {
|
|
id: `${resultCard.id}:step`,
|
|
title: pickAppText(locale, '生成结果', 'Result ready'),
|
|
summary: resultCard.summary || '',
|
|
status: normalizeStatus(resultCard.status),
|
|
createdAt: resultCard.createdAt,
|
|
kind: 'result' as const,
|
|
}
|
|
: null,
|
|
];
|
|
|
|
return [taskStep, ...cardSteps.filter((step): step is TaskUiStep => step !== null)];
|
|
}
|
|
|
|
function buildResult(task: BackendTask, cards: TaskTimelineCard[], locale: AppLocale | string): TaskUiModel['result'] {
|
|
const resultCard = [...cards].reverse().find((card) => card.type === 'result');
|
|
const summary = resultCard?.summary || firstString(task.metadata?.result_summary, task.close_reason);
|
|
const bullets = summary
|
|
.replace(/[。.!?]\s+/g, '\n')
|
|
.split(/\n+/)
|
|
.map((item) => item.trim())
|
|
.filter(Boolean)
|
|
.slice(0, 3);
|
|
return {
|
|
status: normalizeStatus(resultCard?.status || task.status),
|
|
title: resultCard?.title || pickAppText(locale, '本轮结果', 'Current result'),
|
|
summary: summary || '',
|
|
bullets,
|
|
};
|
|
}
|
|
|
|
export function buildTaskUiModel({
|
|
task,
|
|
process,
|
|
cards,
|
|
locale,
|
|
}: {
|
|
task: BackendTask;
|
|
process: SessionProcessProjection;
|
|
cards: TaskTimelineCard[];
|
|
locale: AppLocale | string;
|
|
}): TaskUiModel {
|
|
const steps = buildSteps(task, cards, locale);
|
|
const team = buildTeam(cards);
|
|
const tools = buildTools(cards, process.runs);
|
|
return {
|
|
executionMode: team.hasTeam ? 'team' : 'single',
|
|
team,
|
|
summary: steps[0],
|
|
skills: buildSkills(task, cards, locale),
|
|
tools,
|
|
attempts: buildAttempts(process, cards, tools, locale),
|
|
agentTree: buildAgentTree(process.runs),
|
|
artifacts: buildArtifacts(process),
|
|
steps,
|
|
result: buildResult(task, cards, locale),
|
|
};
|
|
}
|