You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
105 lines
2.7 KiB
105 lines
2.7 KiB
/**
|
|
* A2A 消息格式转换层
|
|
*
|
|
* 负责 A2A 协议类型与内部业务类型之间的双向转换:
|
|
* - Agent 数据行 ↔ Agent Card
|
|
* - A2A Message ↔ 纯文本输入(用于 generateText)
|
|
* - 纯文本输出 ↔ A2A Message
|
|
* - A2A Part ↔ StoredPart(预留,当前仅 TextPart)
|
|
*/
|
|
|
|
import type { AgentRow } from "../agent/types";
|
|
import type { StoredPart } from "../agent/stored-part";
|
|
import type {
|
|
A2AAgentCard,
|
|
A2ASkill,
|
|
A2AMessage,
|
|
A2APart,
|
|
A2ATextPart,
|
|
A2ADataPart,
|
|
A2AFilePart,
|
|
} from "./types";
|
|
import { A2A_PROTOCOL_VERSION } from "./types";
|
|
|
|
// ============ Agent Row → Agent Card ============
|
|
|
|
export function agentRowToCard(agent: AgentRow, baseUrl: string): A2AAgentCard {
|
|
const url = `${baseUrl}/api/agents/${agent.slug}/a2a`;
|
|
|
|
const skills: A2ASkill[] = [
|
|
{
|
|
id: agent.slug,
|
|
name: agent.name,
|
|
description: agent.description ?? agent.systemPrompt.slice(0, 200),
|
|
tags: agent.isCallable ? ["callable", "agent"] : ["agent"],
|
|
inputModes: ["text/plain"],
|
|
outputModes: ["text/plain"],
|
|
},
|
|
];
|
|
|
|
return {
|
|
name: agent.name,
|
|
description: agent.description ?? "",
|
|
url,
|
|
version: "1.0.0",
|
|
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
capabilities: {
|
|
streaming: true,
|
|
pushNotifications: false,
|
|
stateTransition: true,
|
|
},
|
|
defaultInputModes: ["text/plain"],
|
|
defaultOutputModes: ["text/plain"],
|
|
skills,
|
|
};
|
|
}
|
|
|
|
// ============ A2A Message ↔ 纯文本 ============
|
|
|
|
export function a2aMessageToText(message: A2AMessage): string {
|
|
const textParts = message.parts.filter((p): p is A2ATextPart => p.type === "text");
|
|
if (textParts.length === 0) return "";
|
|
return textParts.map((p) => p.text).join("\n");
|
|
}
|
|
|
|
export function textToA2AMessage(text: string, role: "user" | "agent" = "agent"): A2AMessage {
|
|
return {
|
|
role,
|
|
parts: [{ type: "text", text }],
|
|
};
|
|
}
|
|
|
|
// ============ A2A Part ↔ StoredPart(预留) ============
|
|
|
|
export function a2aPartsToStoredParts(parts: A2APart[]): StoredPart[] {
|
|
return parts
|
|
.filter((p): p is A2ATextPart => p.type === "text")
|
|
.map((p, i) => ({
|
|
id: `a2a-part-${i}`,
|
|
type: "text",
|
|
text: p.text,
|
|
}));
|
|
}
|
|
|
|
export function storedPartsToA2AParts(parts: StoredPart[]): A2APart[] {
|
|
return parts
|
|
.filter((p) => p.type === "text" && p.text !== undefined)
|
|
.map((p) => ({
|
|
type: "text" as const,
|
|
text: p.text!,
|
|
}));
|
|
}
|
|
|
|
// ============ Part 类型守卫 ============
|
|
|
|
export function isTextPart(part: A2APart): part is A2ATextPart {
|
|
return part.type === "text";
|
|
}
|
|
|
|
export function isDataPart(part: A2APart): part is A2ADataPart {
|
|
return part.type === "data";
|
|
}
|
|
|
|
export function isFilePart(part: A2APart): part is A2AFilePart {
|
|
return part.type === "file";
|
|
}
|
|
|