|
|
@@ -8,6 +8,21 @@ const DEFAULT_AGENTPAAS_URL = "http://127.0.0.1:8000"
|
|
|
|
|
|
type SSEEvent = { event: string; data: unknown }
|
|
|
type AgentListItem = { id: string; name: string }
|
|
|
+type ProgressStatus = "completed" | "running" | "pending" | "failed"
|
|
|
+type ProgressMilestone = {
|
|
|
+ id: string
|
|
|
+ label: string
|
|
|
+ status: ProgressStatus
|
|
|
+ done?: Set<string>
|
|
|
+ total?: number
|
|
|
+ detail?: string
|
|
|
+}
|
|
|
+type ProgressStage = {
|
|
|
+ id: string
|
|
|
+ label: string
|
|
|
+ status: ProgressStatus
|
|
|
+ milestones: ProgressMilestone[]
|
|
|
+}
|
|
|
|
|
|
export default tool({
|
|
|
description: `把任务转发给远端 AgentPaaS 智能体执行,等待完成后返回最终答案。
|
|
|
@@ -79,9 +94,14 @@ export default tool({
|
|
|
let done: Record<string, unknown> | null = null
|
|
|
let sawCancelled = false
|
|
|
let lastThinkUpdate = 0
|
|
|
+ const progress = createResearch67Progress()
|
|
|
const updateMetadata = (title: string, next: Record<string, unknown>) => {
|
|
|
Object.assign(liveMetadata, next)
|
|
|
- context.metadata({ title, metadata: { ...liveMetadata } })
|
|
|
+ const snapshot = progress.snapshot()
|
|
|
+ context.metadata({
|
|
|
+ title: snapshot?.title ?? title,
|
|
|
+ metadata: { ...liveMetadata, ...(snapshot ? { research67_progress: snapshot } : {}) },
|
|
|
+ })
|
|
|
}
|
|
|
for await (const ev of parseSSE(resp.body)) {
|
|
|
const data = asRecord(ev.data)
|
|
|
@@ -93,6 +113,7 @@ export default tool({
|
|
|
})
|
|
|
} else if (ev.event === "stage_started") {
|
|
|
const stageName = getString(data?.stage_name) || getString(data?.stage_id) || "未知阶段"
|
|
|
+ progress.startStage(getString(data?.stage_id))
|
|
|
updateMetadata(`Research67 正在进行 ${stageName}`, {
|
|
|
pipeline_id: data?.pipeline_id,
|
|
|
stage_id: data?.stage_id,
|
|
|
@@ -103,6 +124,7 @@ export default tool({
|
|
|
})
|
|
|
} else if (ev.event === "stage_passed") {
|
|
|
const stageName = getString(data?.stage_name) || getString(data?.stage_id) || "当前阶段"
|
|
|
+ progress.finishStage(getString(data?.stage_id), "completed")
|
|
|
updateMetadata(`Research67 已通过 ${stageName}`, {
|
|
|
pipeline_id: data?.pipeline_id,
|
|
|
stage_id: data?.stage_id,
|
|
|
@@ -113,6 +135,7 @@ export default tool({
|
|
|
})
|
|
|
} else if (ev.event === "stage_failed") {
|
|
|
const stageName = getString(data?.stage_name) || getString(data?.stage_id) || "当前阶段"
|
|
|
+ progress.finishStage(getString(data?.stage_id), "failed")
|
|
|
updateMetadata(`Research67 阶段失败:${stageName}`, {
|
|
|
pipeline_id: data?.pipeline_id,
|
|
|
stage_id: data?.stage_id,
|
|
|
@@ -124,6 +147,7 @@ export default tool({
|
|
|
status: "failed",
|
|
|
})
|
|
|
} else if (ev.event === "pipeline_completed") {
|
|
|
+ progress.finishPipeline()
|
|
|
updateMetadata("Research67 流水线已完成", {
|
|
|
pipeline_id: data?.pipeline_id,
|
|
|
pipeline_status: data?.status,
|
|
|
@@ -137,6 +161,7 @@ export default tool({
|
|
|
const input = getString(data?.input)
|
|
|
if (!runID) throw new Error("AgentPaaS confirm_required 事件缺少 run_id")
|
|
|
|
|
|
+ progress.note(`等待确认 · ${toolName}`)
|
|
|
updateMetadata(`Research67 正在等待确认:${toolName}`, {
|
|
|
latest_event: ev.event,
|
|
|
pending_confirmation: true,
|
|
|
@@ -156,6 +181,7 @@ export default tool({
|
|
|
approved = false
|
|
|
}
|
|
|
await resolveConfirmation(config, runID, approved, context.abort)
|
|
|
+ progress.note(approved ? "确认已通过,继续执行" : "确认被拒绝,等待流程处理")
|
|
|
updateMetadata(`Research67 已${approved ? "批准" : "拒绝"} ${toolName}`, {
|
|
|
latest_event: "confirmation_resolved",
|
|
|
pending_confirmation: false,
|
|
|
@@ -163,14 +189,16 @@ export default tool({
|
|
|
})
|
|
|
} else if (ev.event === "tool_call") {
|
|
|
const toolName = getString(data?.tool) || "工具"
|
|
|
- updateMetadata(withStage(`Research67 正在调用 ${toolName}`, liveMetadata), {
|
|
|
+ progress.toolCall(toolName, getString(data?.content))
|
|
|
+ updateMetadata(withStage("Research67 正在处理当前步骤", liveMetadata), {
|
|
|
latest_event: ev.event,
|
|
|
latest_tool: toolName,
|
|
|
step: data?.step,
|
|
|
})
|
|
|
} else if (ev.event === "tool_result") {
|
|
|
const toolName = getString(data?.tool) || getString(liveMetadata.latest_tool) || "工具"
|
|
|
- updateMetadata(withStage(`Research67 已完成 ${toolName}`, liveMetadata), {
|
|
|
+ progress.toolResult(toolName, getString(data?.content))
|
|
|
+ updateMetadata(withStage("Research67 已更新当前步骤", liveMetadata), {
|
|
|
latest_event: ev.event,
|
|
|
latest_tool: toolName,
|
|
|
step: data?.step,
|
|
|
@@ -189,9 +217,13 @@ export default tool({
|
|
|
latest_event: ev.event,
|
|
|
step: data?.step,
|
|
|
})
|
|
|
+ } else if (ev.event === "heartbeat") {
|
|
|
+ progress.note("连接正常")
|
|
|
+ updateMetadata(withStage("Research67 仍在运行", liveMetadata), { latest_event: ev.event })
|
|
|
} else if (ev.event === "error") errors.push(extractMessage(ev.data))
|
|
|
else if (ev.event === "cancelled") {
|
|
|
sawCancelled = true
|
|
|
+ progress.cancel()
|
|
|
updateMetadata("Research67 已取消", { latest_event: ev.event, status: "cancelled" })
|
|
|
} else if (ev.event === "done" && data) {
|
|
|
done = data
|
|
|
@@ -203,6 +235,7 @@ export default tool({
|
|
|
throw new Error(`AgentPaaS 任务未正常结束${hint}${errors.length ? ":" + errors.join("; ") : ""}`)
|
|
|
}
|
|
|
const status = typeof done.status === "string" ? done.status : "unknown"
|
|
|
+ const progressMetadata = progress.snapshot()
|
|
|
const metadata = {
|
|
|
agent_name: args.agent_name,
|
|
|
agent_id: agentID,
|
|
|
@@ -213,6 +246,7 @@ export default tool({
|
|
|
total_tokens: done.total_tokens,
|
|
|
cost_usd: done.cost_usd,
|
|
|
workspace_path: done.workspace_path,
|
|
|
+ ...(progressMetadata ? { research67_progress: progressMetadata } : {}),
|
|
|
}
|
|
|
if (status === "completed")
|
|
|
return { title: `AgentPaaS ${args.agent_name}`, output: String(done.output ?? ""), metadata }
|
|
|
@@ -324,6 +358,10 @@ async function* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<SSEEv
|
|
|
buffer = buffer.slice(idx + 2)
|
|
|
const event = parseSSEField(chunk, "event")
|
|
|
const dataRaw = parseSSEField(chunk, "data")
|
|
|
+ if (!event && chunk.split("\n").some((line) => line.trim() === ": heartbeat")) {
|
|
|
+ yield { event: "heartbeat", data: undefined }
|
|
|
+ continue
|
|
|
+ }
|
|
|
let data: unknown = dataRaw
|
|
|
if (dataRaw !== "") {
|
|
|
try {
|
|
|
@@ -366,3 +404,212 @@ function withStage(title: string, metadata: Record<string, unknown>): string {
|
|
|
const stageName = getString(metadata.stage_name)
|
|
|
return stageName ? `${title}(${stageName})` : title
|
|
|
}
|
|
|
+
|
|
|
+function createResearch67Progress() {
|
|
|
+ const stages: ProgressStage[] = [
|
|
|
+ {
|
|
|
+ id: "idea-analysis",
|
|
|
+ label: "idea-analyst · 创意分析",
|
|
|
+ status: "pending",
|
|
|
+ milestones: [
|
|
|
+ { id: "plan", label: "制定分析计划", status: "pending", done: new Set(), total: 1 },
|
|
|
+ { id: "directions", label: "生成检索方向", status: "pending", done: new Set(), total: 2 },
|
|
|
+ { id: "report", label: "形成分析报告", status: "pending", done: new Set(), total: 2 },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "literature-review",
|
|
|
+ label: "lit-searcher · 文献检索",
|
|
|
+ status: "pending",
|
|
|
+ milestones: [
|
|
|
+ { id: "plan", label: "制定检索计划", status: "pending", done: new Set(), total: 1 },
|
|
|
+ { id: "search", label: "检索候选文献", status: "pending", done: new Set(), detail: "尚未开始" },
|
|
|
+ { id: "select", label: "筛选入选论文", status: "pending", done: new Set(), total: 1 },
|
|
|
+ { id: "report", label: "形成文献综述", status: "pending", done: new Set(), total: 2 },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "paper-writing",
|
|
|
+ label: "paper-writer · 论文撰写",
|
|
|
+ status: "pending",
|
|
|
+ milestones: [
|
|
|
+ { id: "outline", label: "写作计划与大纲", status: "pending", done: new Set(), total: 2 },
|
|
|
+ { id: "english", label: "英文稿与参考文献", status: "pending", done: new Set(), total: 8 },
|
|
|
+ { id: "chinese", label: "中文稿", status: "pending", done: new Set(), total: 7 },
|
|
|
+ { id: "compile", label: "编译双语 PDF", status: "pending", done: new Set(), total: 2 },
|
|
|
+ { id: "report", label: "形成交付报告", status: "pending", done: new Set(), total: 2 },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ let current: ProgressStage | undefined
|
|
|
+ let active: ProgressMilestone | undefined
|
|
|
+ let pendingArtifact: { milestone: ProgressMilestone; key: string } | undefined
|
|
|
+ let searchCalls = 0
|
|
|
+ let note = ""
|
|
|
+ let activeSince = Date.now()
|
|
|
+
|
|
|
+ const startMilestone = (milestone: ProgressMilestone) => {
|
|
|
+ if (!current) return
|
|
|
+ for (const item of current.milestones) {
|
|
|
+ if (item === milestone) break
|
|
|
+ item.status = "completed"
|
|
|
+ }
|
|
|
+ if (milestone.status !== "completed") milestone.status = "running"
|
|
|
+ active = milestone
|
|
|
+ activeSince = Date.now()
|
|
|
+ note = ""
|
|
|
+ }
|
|
|
+
|
|
|
+ const artifact = (content: string) => {
|
|
|
+ if (!current) return undefined
|
|
|
+ const value = content.replaceAll("\\", "/").toLowerCase()
|
|
|
+ const match = (milestoneID: string, key: string) => {
|
|
|
+ const milestone = current?.milestones.find((item) => item.id === milestoneID)
|
|
|
+ return milestone ? { milestone, key } : undefined
|
|
|
+ }
|
|
|
+ if (value.includes("/work_plan.md") || value.includes('"work_plan.md"')) {
|
|
|
+ return match(current.id === "paper-writing" ? "outline" : "plan", "work_plan")
|
|
|
+ }
|
|
|
+ if (current.id === "idea-analysis") {
|
|
|
+ if (value.includes("search_queries.json")) return match("directions", "search_queries")
|
|
|
+ if (value.includes("similar_papers.json")) return match("directions", "similar_papers")
|
|
|
+ if (value.includes("report.json")) return match("report", "report_json")
|
|
|
+ if (value.includes("report.md")) return match("report", "report_md")
|
|
|
+ }
|
|
|
+ if (current.id === "literature-review") {
|
|
|
+ if (value.includes("papers_selected.json")) return match("select", "papers_selected")
|
|
|
+ if (value.includes("report.json")) return match("report", "report_json")
|
|
|
+ if (value.includes("report.md")) return match("report", "report_md")
|
|
|
+ }
|
|
|
+ if (current.id !== "paper-writing") return undefined
|
|
|
+ if (value.includes("outline.json")) return match("outline", "outline")
|
|
|
+ const section = value.match(
|
|
|
+ /artifacts\/sections\/(abstract|introduction|related_work|method|experiments|conclusion)\.tex/,
|
|
|
+ )
|
|
|
+ if (section) return match("english", `section:${section[1]}`)
|
|
|
+ if (value.includes("paper.tex")) return match("english", "paper_tex")
|
|
|
+ if (value.includes("references.bib")) return match("english", "references")
|
|
|
+ const sectionZh = value.match(
|
|
|
+ /artifacts\/sections_zh\/(abstract|introduction|related_work|method|experiments|conclusion)\.tex/,
|
|
|
+ )
|
|
|
+ if (sectionZh) return match("chinese", `section:${sectionZh[1]}`)
|
|
|
+ if (value.includes("paper_zh.tex")) return match("chinese", "paper_zh_tex")
|
|
|
+ if (value.includes("report.json")) return match("report", "report_json")
|
|
|
+ if (value.includes("report.md")) return match("report", "report_md")
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+
|
|
|
+ const completeArtifact = (item: { milestone: ProgressMilestone; key: string }) => {
|
|
|
+ item.milestone.done?.add(item.key)
|
|
|
+ if (item.milestone.total && item.milestone.done?.size === item.milestone.total) {
|
|
|
+ item.milestone.status = "completed"
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ startStage(stageID: string) {
|
|
|
+ current = stages.find((stage) => stage.id === stageID)
|
|
|
+ if (!current) return
|
|
|
+ for (const stage of stages) {
|
|
|
+ if (stage === current) break
|
|
|
+ stage.status = "completed"
|
|
|
+ }
|
|
|
+ current.status = "running"
|
|
|
+ startMilestone(current.milestones[0])
|
|
|
+ },
|
|
|
+ finishStage(stageID: string, status: "completed" | "failed") {
|
|
|
+ const stage = stages.find((item) => item.id === stageID)
|
|
|
+ if (!stage) return
|
|
|
+ stage.status = status
|
|
|
+ for (const milestone of stage.milestones) milestone.status = status
|
|
|
+ current = stage
|
|
|
+ active = status === "failed" ? active : undefined
|
|
|
+ note = status === "failed" ? "阶段执行失败" : "阶段验收通过"
|
|
|
+ },
|
|
|
+ finishPipeline() {
|
|
|
+ for (const stage of stages) {
|
|
|
+ stage.status = "completed"
|
|
|
+ for (const milestone of stage.milestones) milestone.status = "completed"
|
|
|
+ }
|
|
|
+ active = undefined
|
|
|
+ note = "全部阶段已完成"
|
|
|
+ },
|
|
|
+ cancel() {
|
|
|
+ if (current?.status === "running") current.status = "failed"
|
|
|
+ if (active?.status === "running") active.status = "failed"
|
|
|
+ note = "任务已取消"
|
|
|
+ },
|
|
|
+ note(value: string) {
|
|
|
+ note = value
|
|
|
+ },
|
|
|
+ toolCall(toolName: string, content: string) {
|
|
|
+ const lower = toolName.toLowerCase()
|
|
|
+ if (current?.id === "literature-review" && ["arxiv_search", "openalex_search"].includes(lower)) {
|
|
|
+ const milestone = current.milestones.find((item) => item.id === "search")!
|
|
|
+ startMilestone(milestone)
|
|
|
+ pendingArtifact = undefined
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (current?.id === "paper-writing" && lower === "compilelatex") {
|
|
|
+ const milestone = current.milestones.find((item) => item.id === "compile")!
|
|
|
+ startMilestone(milestone)
|
|
|
+ pendingArtifact = {
|
|
|
+ milestone,
|
|
|
+ key: content.toLowerCase().includes("paper_zh.tex") ? "paper_zh_pdf" : "paper_pdf",
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const item = artifact(content)
|
|
|
+ if (!item) return
|
|
|
+ startMilestone(item.milestone)
|
|
|
+ pendingArtifact = item
|
|
|
+ },
|
|
|
+ toolResult(toolName: string, content: string) {
|
|
|
+ const lower = toolName.toLowerCase()
|
|
|
+ const failed = /\[error\]|mcp_error|"status"\s*:\s*"(?:failed|error)"/i.test(content)
|
|
|
+ if (current?.id === "literature-review" && ["arxiv_search", "openalex_search"].includes(lower)) {
|
|
|
+ if (!failed) searchCalls++
|
|
|
+ const milestone = current.milestones.find((item) => item.id === "search")!
|
|
|
+ milestone.detail = searchCalls ? `已完成 ${searchCalls} 轮检索` : "等待检索重试"
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!pendingArtifact) return
|
|
|
+ if (!failed) completeArtifact(pendingArtifact)
|
|
|
+ if (failed) note = "当前操作失败,等待重试"
|
|
|
+ pendingArtifact = undefined
|
|
|
+ },
|
|
|
+ snapshot() {
|
|
|
+ if (!current) return undefined
|
|
|
+ const elapsed = Math.max(0, Math.floor((Date.now() - activeSince) / 1000))
|
|
|
+ const rows = stages.flatMap((stage) => {
|
|
|
+ const detail = stage.status === "running" ? active?.label : undefined
|
|
|
+ const parent = [{ id: stage.id, label: stage.label, status: stage.status, detail, depth: 0 }]
|
|
|
+ if (stage !== current || !["running", "failed"].includes(stage.status)) return parent
|
|
|
+ return parent.concat(
|
|
|
+ stage.milestones.map((milestone) => ({
|
|
|
+ id: `${stage.id}:${milestone.id}`,
|
|
|
+ label: milestone.label,
|
|
|
+ status: milestone.status,
|
|
|
+ detail:
|
|
|
+ milestone.detail ??
|
|
|
+ (milestone.status !== "completed" && milestone.total && milestone.done?.size
|
|
|
+ ? `${milestone.done.size}/${milestone.total}`
|
|
|
+ : undefined),
|
|
|
+ depth: 1,
|
|
|
+ })),
|
|
|
+ )
|
|
|
+ })
|
|
|
+ return {
|
|
|
+ kind: "research67",
|
|
|
+ title: active ? `Research67 · ${current.label} · ${active.label}` : `Research67 · ${current.label}`,
|
|
|
+ rows,
|
|
|
+ footer: note || (active ? `当前步骤已运行 ${formatElapsed(elapsed)}` : "进度已更新"),
|
|
|
+ }
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function formatElapsed(seconds: number) {
|
|
|
+ if (seconds < 60) return `${seconds} 秒`
|
|
|
+ return `${Math.floor(seconds / 60)} 分 ${seconds % 60} 秒`
|
|
|
+}
|