feat(engine): 添加技能查看工具并优化异步任务管理 - 添加SkillViewTool到引擎加载器中,增强技能管理功能 - 在AgentLoop中引入_active_direct_task来跟踪活跃任务 - 实现直接任务执行时的同步处理逻辑 - 更新工具实例化方式以支持依赖注入 feat(config): 增加智能体运行时参数配置支持 - 扩展AgentDefaultsConfig添加max_tokens和temperature字段 - 实现配置解析函数_first_config_value处理多个配置源 - 支持通过Web API动态更新智能体运行时参数 - 添加前端页面配置表单和验证逻辑 refactor(provider): 统一最大令牌数参数类型为可选整型 - 将所有LLM提供者的max_tokens参数改为int | None类型 - 为AnthropicProvider实现模型特定的最大令牌数默认值 - 调整参数传递逻辑,优先级:调用参数 > 配置文件 > 模型默认值 - 移除硬编码的默认值,改用条件判断 feat(process): 增强事件投影功能 - 添加工具调用开始/结束事件的映射逻辑 - 实现技能激活事件的识别和展示 - 添加辅助函数处理工具调用名称和参数提取 - 优化运行记录关联逻辑,提升事件匹配准确性 fix(web): 更新网络请求客户端信任环境设置 - 将WebFetchTool和WebSearchTool的trust_env参数设为True - 确保HTTP客户端能够正确使用系统代理配置 - 修复可能的网络连接问题 test: 添加配置加载和事件投影相关测试 - 新增智能体默认参数配置测试用例 - 实现API配置持久化和重载测试 - 添加技能卡片和工具事件的投影测试 ```
491 lines
15 KiB
TypeScript
491 lines
15 KiB
TypeScript
import type {
|
|
BackendTask,
|
|
ProcessArtifact,
|
|
ProcessEvent,
|
|
ProcessRun,
|
|
TaskTimelineCard,
|
|
TaskTimelineCardType,
|
|
} from '@/types';
|
|
|
|
export type BuildTaskTimelineCardsInput = {
|
|
task: BackendTask;
|
|
processRuns?: ProcessRun[];
|
|
processEvents?: ProcessEvent[];
|
|
processArtifacts?: ProcessArtifact[];
|
|
};
|
|
|
|
const TIMELINE_CARD_TYPES = new Set<TaskTimelineCardType>([
|
|
'task_created',
|
|
'plan',
|
|
'skill',
|
|
'tool_call',
|
|
'tool_result',
|
|
'next_step',
|
|
'agent_team',
|
|
'agent_progress',
|
|
'agent_handoff',
|
|
'artifact',
|
|
'error',
|
|
'result',
|
|
'result_history',
|
|
'acceptance',
|
|
]);
|
|
|
|
const RESULT_STATUSES = new Set(['awaiting_acceptance', 'closed', 'abandoned', 'cancelled', 'error']);
|
|
|
|
function isTimelineCardType(value: unknown): value is TaskTimelineCardType {
|
|
return typeof value === 'string' && TIMELINE_CARD_TYPES.has(value as TaskTimelineCardType);
|
|
}
|
|
|
|
function toTime(value: string): number | null {
|
|
const parsed = new Date(value).getTime();
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
|
|
function firstString(...values: unknown[]): string | undefined {
|
|
for (const value of values) {
|
|
if (typeof value !== 'string') continue;
|
|
const trimmed = value.trim();
|
|
if (trimmed) return trimmed;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function stringList(value: unknown): string[] {
|
|
if (Array.isArray(value)) {
|
|
return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0);
|
|
}
|
|
if (typeof value === 'string' && value.trim()) {
|
|
return [value.trim()];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function normalizeSkillNames(metadata: Record<string, unknown> | undefined): string[] | undefined {
|
|
if (!metadata || (!('skill_names' in metadata) && !('selected_skill_names' in metadata))) {
|
|
return undefined;
|
|
}
|
|
const names = [
|
|
...stringList(metadata.skill_names),
|
|
...stringList(metadata.selected_skill_names),
|
|
];
|
|
return Array.from(new Set(names));
|
|
}
|
|
|
|
function cardTypeForEvent(event: ProcessEvent): TaskTimelineCardType | null {
|
|
const timelineType = event.metadata?.timeline_type;
|
|
if (isTimelineCardType(timelineType)) {
|
|
return timelineType;
|
|
}
|
|
|
|
switch (String(event.kind)) {
|
|
case 'task_planned':
|
|
case 'run_started':
|
|
return 'plan';
|
|
case 'skill_selected':
|
|
return 'skill';
|
|
case 'tool_call_started':
|
|
return 'tool_call';
|
|
case 'tool_call_finished':
|
|
return 'tool_result';
|
|
case 'agent_team_created':
|
|
return 'agent_team';
|
|
case 'agent_handoff':
|
|
return 'agent_handoff';
|
|
case 'agent_finished':
|
|
case 'run_progress':
|
|
case 'run_finished':
|
|
return 'agent_progress';
|
|
case 'task_result_ready':
|
|
return 'result';
|
|
case 'task_acceptance_recorded':
|
|
return 'acceptance';
|
|
case 'task_error':
|
|
return 'error';
|
|
default:
|
|
if (event.status === 'error') {
|
|
return 'error';
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function titleForCard(type: TaskTimelineCardType, actorName?: string): string {
|
|
switch (type) {
|
|
case 'task_created':
|
|
return '任务已创建';
|
|
case 'plan':
|
|
return '执行计划';
|
|
case 'skill':
|
|
return '选择 Skill';
|
|
case 'tool_call':
|
|
return actorName ? `调用工具:${actorName}` : '调用工具';
|
|
case 'tool_result':
|
|
return actorName ? `工具结果:${actorName}` : '工具结果';
|
|
case 'next_step':
|
|
return '下一步';
|
|
case 'agent_team':
|
|
return '启动 Agent Team';
|
|
case 'agent_progress':
|
|
return actorName || 'Agent 进展';
|
|
case 'agent_handoff':
|
|
return 'Agent 交接';
|
|
case 'artifact':
|
|
return '生成产物';
|
|
case 'error':
|
|
return '执行遇到问题';
|
|
case 'result':
|
|
return '本轮结果';
|
|
case 'result_history':
|
|
return '历史结果版本';
|
|
case 'acceptance':
|
|
return '任务验收';
|
|
}
|
|
}
|
|
|
|
function summaryForEvent(event: ProcessEvent): string | undefined {
|
|
return firstString(
|
|
event.metadata?.result_summary,
|
|
event.metadata?.reason,
|
|
event.metadata?.action_summary,
|
|
event.text,
|
|
);
|
|
}
|
|
|
|
function detailsForEvent(event: ProcessEvent): Record<string, unknown> | undefined {
|
|
const skillNames = normalizeSkillNames(event.metadata);
|
|
if (!event.metadata && !skillNames) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...(event.metadata ?? {}),
|
|
...(skillNames ? { skill_names: skillNames } : {}),
|
|
};
|
|
}
|
|
|
|
function feedbackCreatedAt(feedback: Record<string, unknown>, task: BackendTask): string {
|
|
return firstString(feedback.created_at, task.updated_at, task.created_at) ?? task.created_at;
|
|
}
|
|
|
|
function feedbackSummary(feedback: Record<string, unknown>): string | undefined {
|
|
return firstString(feedback.comment, feedback.summary, feedback.acceptance_type);
|
|
}
|
|
|
|
function acceptanceTypeFromRecord(record: Record<string, unknown> | undefined): string | null {
|
|
return firstString(record?.acceptance_type, record?.feedback_type)?.toLowerCase() ?? null;
|
|
}
|
|
|
|
function resultSummary(task: BackendTask): string | undefined {
|
|
return firstString(
|
|
task.metadata?.result_summary,
|
|
task.metadata?.summary,
|
|
task.close_reason,
|
|
task.validation_result?.summary,
|
|
);
|
|
}
|
|
|
|
function assistantResultForRun(task: BackendTask, runId: string | null | undefined): string | undefined {
|
|
if (!runId) return undefined;
|
|
const run = (task.runs ?? []).find((item) => item.run_id === runId);
|
|
if (!run) return undefined;
|
|
const assistantMessages = run.messages.filter((message) => message.role === 'assistant' && message.content.trim());
|
|
return lastItem(assistantMessages)?.content.trim();
|
|
}
|
|
|
|
function resultSummaryForEvent(task: BackendTask, event: ProcessEvent): string | undefined {
|
|
return firstString(assistantResultForRun(task, event.run_id), summaryForEvent(event));
|
|
}
|
|
|
|
function fallbackResultSummary(task: BackendTask): string | undefined {
|
|
return firstString(assistantResultForRun(task, lastItem(task.run_ids)), resultSummary(task));
|
|
}
|
|
|
|
function buildRunMap(processRuns: ProcessRun[]): Map<string, ProcessRun> {
|
|
const map = new Map<string, ProcessRun>();
|
|
for (const run of processRuns) {
|
|
map.set(run.run_id, run);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
function lastItem<T>(items: T[]): T | null {
|
|
return items.length > 0 ? items[items.length - 1] : null;
|
|
}
|
|
|
|
function compareCardsByCreatedAt(
|
|
a: { card: TaskTimelineCard; index: number },
|
|
b: { card: TaskTimelineCard; index: number },
|
|
): number {
|
|
const aTime = toTime(a.card.createdAt);
|
|
const bTime = toTime(b.card.createdAt);
|
|
|
|
if (aTime === null && bTime === null) {
|
|
return a.index - b.index;
|
|
}
|
|
if (aTime === null) {
|
|
return 1;
|
|
}
|
|
if (bTime === null) {
|
|
return -1;
|
|
}
|
|
|
|
return aTime - bTime || a.index - b.index;
|
|
}
|
|
|
|
type AcceptanceEventIdentity = {
|
|
runId: string | null;
|
|
acceptanceType: string | null;
|
|
};
|
|
|
|
function isCoveredByAcceptanceEvent(
|
|
feedback: Record<string, unknown>,
|
|
acceptanceEvents: AcceptanceEventIdentity[],
|
|
): boolean {
|
|
const feedbackType = acceptanceTypeFromRecord(feedback);
|
|
if (!feedbackType) return false;
|
|
|
|
const feedbackRunId = firstString(feedback.run_id) ?? null;
|
|
const matchingTypeEvents = acceptanceEvents.filter((event) => event.acceptanceType === feedbackType);
|
|
|
|
if (feedbackRunId) {
|
|
return (
|
|
matchingTypeEvents.some((event) => event.runId === feedbackRunId) ||
|
|
(matchingTypeEvents.length === 1 && !matchingTypeEvents[0].runId)
|
|
);
|
|
}
|
|
|
|
return matchingTypeEvents.length === 1;
|
|
}
|
|
|
|
function cardTime(card: TaskTimelineCard): number {
|
|
return toTime(card.createdAt) ?? Number.MAX_SAFE_INTEGER;
|
|
}
|
|
|
|
function cardComment(card: TaskTimelineCard): string | undefined {
|
|
return firstString(card.details?.comment, card.summary);
|
|
}
|
|
|
|
function toolCallKeyFromEvent(event: ProcessEvent): string | null {
|
|
const toolCallId = firstString(event.metadata?.tool_call_id);
|
|
if (toolCallId) return `${event.run_id}:${toolCallId}`;
|
|
|
|
const toolName = firstString(event.metadata?.tool_name, event.actor_name, event.actor_id);
|
|
if (toolName) return `${event.run_id}:${toolName}`;
|
|
|
|
return null;
|
|
}
|
|
|
|
function buildToolResultStatusByCall(processEvents: ProcessEvent[]): Map<string, string> {
|
|
const statuses = new Map<string, string>();
|
|
for (const event of processEvents) {
|
|
if (cardTypeForEvent(event) !== 'tool_result') continue;
|
|
const key = toolCallKeyFromEvent(event);
|
|
if (!key) continue;
|
|
statuses.set(key, event.status || 'done');
|
|
}
|
|
return statuses;
|
|
}
|
|
|
|
function buildResultHistoryCard(task: BackendTask, resultCards: TaskTimelineCard[], acceptanceCards: TaskTimelineCard[]): TaskTimelineCard {
|
|
const versions = resultCards.map((resultCard) => {
|
|
const acceptanceCard = acceptanceCards
|
|
.filter((card) => card.runId === resultCard.runId)
|
|
.sort((a, b) => cardTime(a) - cardTime(b))
|
|
.at(-1);
|
|
return {
|
|
runId: resultCard.runId ?? null,
|
|
result: resultCard.summary ?? '',
|
|
createdAt: resultCard.createdAt,
|
|
status: acceptanceCard?.status ?? resultCard.status ?? null,
|
|
acceptanceType: acceptanceCard?.status ?? null,
|
|
comment: acceptanceCard ? cardComment(acceptanceCard) ?? '' : '',
|
|
acceptedAt: acceptanceCard?.createdAt ?? null,
|
|
};
|
|
});
|
|
|
|
return {
|
|
id: `${task.task_id}:result-history`,
|
|
taskId: task.task_id,
|
|
type: 'result_history',
|
|
title: titleForCard('result_history'),
|
|
summary: `${resultCards.length} 历史结果版本`,
|
|
createdAt: resultCards[0]?.createdAt ?? task.created_at,
|
|
details: { versions },
|
|
};
|
|
}
|
|
|
|
function collapseHistoricalResults(task: BackendTask, cards: TaskTimelineCard[]): TaskTimelineCard[] {
|
|
const resultCards = cards.filter((card) => card.type === 'result');
|
|
if (resultCards.length <= 1) return cards;
|
|
|
|
const finalAcceptedRunId = firstString(task.metadata?.final_accepted_run_id);
|
|
const latestResult =
|
|
(finalAcceptedRunId ? resultCards.find((card) => card.runId === finalAcceptedRunId) : undefined) ??
|
|
[...resultCards].sort((a, b) => cardTime(a) - cardTime(b)).at(-1);
|
|
if (!latestResult) return cards;
|
|
|
|
const oldResults = resultCards
|
|
.filter((card) => card.id !== latestResult.id)
|
|
.sort((a, b) => cardTime(a) - cardTime(b));
|
|
if (oldResults.length === 0) return cards;
|
|
|
|
const oldRunIds = new Set(oldResults.map((card) => card.runId).filter(Boolean));
|
|
const oldAcceptances = cards
|
|
.filter((card) => card.type === 'acceptance' && oldRunIds.has(card.runId))
|
|
.sort((a, b) => cardTime(a) - cardTime(b));
|
|
const foldedIds = new Set([...oldResults, ...oldAcceptances].map((card) => card.id));
|
|
const historyCard = buildResultHistoryCard(task, oldResults, oldAcceptances);
|
|
const firstOldResultIndex = cards.findIndex((card) => card.id === oldResults[0].id);
|
|
const output: TaskTimelineCard[] = [];
|
|
|
|
for (let index = 0; index < cards.length; index += 1) {
|
|
if (index === firstOldResultIndex) {
|
|
output.push(historyCard);
|
|
}
|
|
if (!foldedIds.has(cards[index].id)) {
|
|
output.push(cards[index]);
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
export function buildTaskTimelineCards(input: BuildTaskTimelineCardsInput): TaskTimelineCard[] {
|
|
const { task } = input;
|
|
const processRuns = input.processRuns ?? task.process_runs ?? [];
|
|
const processEvents = input.processEvents ?? task.process_events ?? [];
|
|
const processArtifacts = input.processArtifacts ?? task.process_artifacts ?? [];
|
|
const runsById = buildRunMap(processRuns);
|
|
const toolResultStatusByCall = buildToolResultStatusByCall(processEvents);
|
|
const runsWithProgressEvents = new Set<string>();
|
|
const acceptanceEvents: AcceptanceEventIdentity[] = [];
|
|
let hasResultEventCard = false;
|
|
const cards: TaskTimelineCard[] = [
|
|
{
|
|
id: `${task.task_id}:created`,
|
|
taskId: task.task_id,
|
|
type: 'task_created',
|
|
title: titleForCard('task_created'),
|
|
summary: firstString(task.short_title, task.description, task.goal),
|
|
actorName: task.creator,
|
|
status: task.status,
|
|
createdAt: task.created_at,
|
|
details: task.metadata,
|
|
},
|
|
];
|
|
|
|
for (const event of processEvents) {
|
|
const type = cardTypeForEvent(event);
|
|
if (!type) continue;
|
|
if (type === 'agent_progress') {
|
|
runsWithProgressEvents.add(event.run_id);
|
|
}
|
|
if (type === 'result') {
|
|
hasResultEventCard = true;
|
|
}
|
|
if (type === 'acceptance') {
|
|
acceptanceEvents.push({
|
|
runId: firstString(event.run_id) ?? null,
|
|
acceptanceType: acceptanceTypeFromRecord(event.metadata),
|
|
});
|
|
}
|
|
|
|
cards.push({
|
|
id: event.event_id,
|
|
taskId: task.task_id,
|
|
runId: event.run_id,
|
|
parentRunId: event.parent_run_id,
|
|
type,
|
|
title: titleForCard(type, event.actor_name),
|
|
summary: type === 'result' ? resultSummaryForEvent(task, event) : summaryForEvent(event),
|
|
actorName: event.actor_name,
|
|
status:
|
|
type === 'tool_call'
|
|
? toolResultStatusByCall.get(toolCallKeyFromEvent(event) ?? '') ?? event.status
|
|
: event.status,
|
|
createdAt: event.created_at,
|
|
details: detailsForEvent(event),
|
|
});
|
|
}
|
|
|
|
for (const run of processRuns) {
|
|
if (!run.parent_run_id) continue;
|
|
if (runsWithProgressEvents.has(run.run_id)) continue;
|
|
|
|
cards.push({
|
|
id: `${run.run_id}:fallback-progress`,
|
|
taskId: task.task_id,
|
|
runId: run.run_id,
|
|
parentRunId: run.parent_run_id,
|
|
type: 'agent_progress',
|
|
title: titleForCard('agent_progress', run.actor_name),
|
|
summary: firstString(run.summary, run.title),
|
|
actorName: run.actor_name,
|
|
status: run.status,
|
|
createdAt: run.started_at,
|
|
details: run.metadata,
|
|
});
|
|
}
|
|
|
|
for (const artifact of processArtifacts) {
|
|
const run = runsById.get(artifact.run_id);
|
|
cards.push({
|
|
id: artifact.artifact_id,
|
|
taskId: task.task_id,
|
|
runId: artifact.run_id,
|
|
parentRunId: run?.parent_run_id,
|
|
type: 'artifact',
|
|
title: titleForCard('artifact'),
|
|
summary: firstString(artifact.title),
|
|
actorName: artifact.actor_name,
|
|
createdAt: artifact.created_at,
|
|
relatedArtifactIds: [artifact.artifact_id],
|
|
details: {
|
|
...(artifact.metadata ?? {}),
|
|
artifact_type: artifact.artifact_type,
|
|
title: artifact.title,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (RESULT_STATUSES.has(task.status) && !hasResultEventCard) {
|
|
cards.push({
|
|
id: `${task.task_id}:result`,
|
|
taskId: task.task_id,
|
|
runId: lastItem(task.run_ids),
|
|
type: 'result',
|
|
title: titleForCard('result'),
|
|
summary: fallbackResultSummary(task),
|
|
status: task.status,
|
|
createdAt: task.closed_at ?? task.updated_at ?? task.created_at,
|
|
details: task.validation_result ?? undefined,
|
|
});
|
|
}
|
|
|
|
for (let index = 0; index < task.feedback.length; index += 1) {
|
|
const feedback = task.feedback[index];
|
|
const runId = firstString(feedback.run_id) ?? null;
|
|
const createdAt = feedbackCreatedAt(feedback, task);
|
|
if (isCoveredByAcceptanceEvent(feedback, acceptanceEvents)) continue;
|
|
|
|
cards.push({
|
|
id: `${task.task_id}:acceptance:${index}`,
|
|
taskId: task.task_id,
|
|
runId,
|
|
type: 'acceptance',
|
|
title: titleForCard('acceptance'),
|
|
summary: feedbackSummary(feedback),
|
|
status: firstString(feedback.acceptance_type),
|
|
createdAt,
|
|
details: feedback,
|
|
});
|
|
}
|
|
|
|
const sortedCards = cards
|
|
.map((card, index) => ({ card, index }))
|
|
.sort(compareCardsByCreatedAt)
|
|
.map(({ card }) => card);
|
|
|
|
return collapseHistoricalResults(task, sortedCards);
|
|
}
|