|
@@ -92,6 +92,7 @@ export default tool({
|
|
|
status: "running",
|
|
status: "running",
|
|
|
}
|
|
}
|
|
|
let done: Record<string, unknown> | null = null
|
|
let done: Record<string, unknown> | null = null
|
|
|
|
|
+ let startedRunID = ""
|
|
|
let sawCancelled = false
|
|
let sawCancelled = false
|
|
|
let lastThinkUpdate = 0
|
|
let lastThinkUpdate = 0
|
|
|
const progress = createResearch67Progress()
|
|
const progress = createResearch67Progress()
|
|
@@ -103,9 +104,17 @@ export default tool({
|
|
|
metadata: { ...liveMetadata, ...(snapshot ? { research67_progress: snapshot } : {}) },
|
|
metadata: { ...liveMetadata, ...(snapshot ? { research67_progress: snapshot } : {}) },
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
+ let remoteCancellation: Promise<void> | undefined
|
|
|
|
|
+ const cancelOnAbort = () => {
|
|
|
|
|
+ if (!startedRunID || remoteCancellation) return
|
|
|
|
|
+ remoteCancellation = cancelRemoteRun(config, agentID, startedRunID).catch(() => undefined)
|
|
|
|
|
+ }
|
|
|
|
|
+ context.abort.addEventListener("abort", cancelOnAbort, { once: true })
|
|
|
for await (const ev of parseSSE(resp.body)) {
|
|
for await (const ev of parseSSE(resp.body)) {
|
|
|
const data = asRecord(ev.data)
|
|
const data = asRecord(ev.data)
|
|
|
if (ev.event === "started") {
|
|
if (ev.event === "started") {
|
|
|
|
|
+ startedRunID = getString(data?.run_id)
|
|
|
|
|
+ if (context.abort.aborted) cancelOnAbort()
|
|
|
updateMetadata("Research67 正在运行", {
|
|
updateMetadata("Research67 正在运行", {
|
|
|
run_id: data?.run_id,
|
|
run_id: data?.run_id,
|
|
|
thread_id: data?.thread_id,
|
|
thread_id: data?.thread_id,
|
|
@@ -220,8 +229,17 @@ export default tool({
|
|
|
} else if (ev.event === "heartbeat") {
|
|
} else if (ev.event === "heartbeat") {
|
|
|
progress.note("连接正常")
|
|
progress.note("连接正常")
|
|
|
updateMetadata(withStage("Research67 仍在运行", liveMetadata), { latest_event: ev.event })
|
|
updateMetadata(withStage("Research67 仍在运行", liveMetadata), { latest_event: ev.event })
|
|
|
- } else if (ev.event === "error") errors.push(extractMessage(ev.data))
|
|
|
|
|
- else if (ev.event === "cancelled") {
|
|
|
|
|
|
|
+ } else if (ev.event === "error") {
|
|
|
|
|
+ const message = extractMessage(ev.data)
|
|
|
|
|
+ errors.push(message)
|
|
|
|
|
+ updateMetadata("Research67 执行失败", {
|
|
|
|
|
+ latest_event: ev.event,
|
|
|
|
|
+ status: "failed",
|
|
|
|
|
+ error_code: data?.code,
|
|
|
|
|
+ error_message: message,
|
|
|
|
|
+ workspace_path: data?.workspace_path ?? liveMetadata.workspace_path,
|
|
|
|
|
+ })
|
|
|
|
|
+ } else if (ev.event === "cancelled") {
|
|
|
sawCancelled = true
|
|
sawCancelled = true
|
|
|
progress.cancel()
|
|
progress.cancel()
|
|
|
updateMetadata("Research67 已取消", { latest_event: ev.event, status: "cancelled" })
|
|
updateMetadata("Research67 已取消", { latest_event: ev.event, status: "cancelled" })
|
|
@@ -230,6 +248,7 @@ export default tool({
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ context.abort.removeEventListener("abort", cancelOnAbort)
|
|
|
if (!done) {
|
|
if (!done) {
|
|
|
const hint = sawCancelled ? "(收到 cancelled 事件)" : "(未收到 done 事件,连接可能中断)"
|
|
const hint = sawCancelled ? "(收到 cancelled 事件)" : "(未收到 done 事件,连接可能中断)"
|
|
|
throw new Error(`AgentPaaS 任务未正常结束${hint}${errors.length ? ":" + errors.join("; ") : ""}`)
|
|
throw new Error(`AgentPaaS 任务未正常结束${hint}${errors.length ? ":" + errors.join("; ") : ""}`)
|
|
@@ -239,13 +258,15 @@ export default tool({
|
|
|
const metadata = {
|
|
const metadata = {
|
|
|
agent_name: args.agent_name,
|
|
agent_name: args.agent_name,
|
|
|
agent_id: agentID,
|
|
agent_id: agentID,
|
|
|
- run_id: done.run_id,
|
|
|
|
|
- thread_id: done.thread_id,
|
|
|
|
|
|
|
+ run_id: done.run_id ?? liveMetadata.run_id,
|
|
|
|
|
+ thread_id: done.thread_id ?? liveMetadata.thread_id,
|
|
|
status,
|
|
status,
|
|
|
steps: done.steps,
|
|
steps: done.steps,
|
|
|
total_tokens: done.total_tokens,
|
|
total_tokens: done.total_tokens,
|
|
|
cost_usd: done.cost_usd,
|
|
cost_usd: done.cost_usd,
|
|
|
- workspace_path: done.workspace_path,
|
|
|
|
|
|
|
+ workspace_path: done.workspace_path ?? liveMetadata.workspace_path,
|
|
|
|
|
+ error_code: liveMetadata.error_code,
|
|
|
|
|
+ error_message: liveMetadata.error_message,
|
|
|
...(progressMetadata ? { research67_progress: progressMetadata } : {}),
|
|
...(progressMetadata ? { research67_progress: progressMetadata } : {}),
|
|
|
}
|
|
}
|
|
|
if (status === "completed")
|
|
if (status === "completed")
|
|
@@ -253,7 +274,13 @@ export default tool({
|
|
|
if (status === "cancelled") return { title: `AgentPaaS ${args.agent_name}`, output: "任务已被取消。", metadata }
|
|
if (status === "cancelled") return { title: `AgentPaaS ${args.agent_name}`, output: "任务已被取消。", metadata }
|
|
|
return {
|
|
return {
|
|
|
title: `AgentPaaS ${args.agent_name}(失败)`,
|
|
title: `AgentPaaS ${args.agent_name}(失败)`,
|
|
|
- output: `[AgentPaaS 执行失败] ${errors.join("; ") || String(done.output ?? "") || "未知错误"}`,
|
|
|
|
|
|
|
+ output: [
|
|
|
|
|
+ `[AgentPaaS 执行失败] ${errors.join("; ") || String(done.output ?? "") || "未知错误"}`,
|
|
|
|
|
+ metadata.workspace_path
|
|
|
|
|
+ ? `已有产物保留在:${metadata.workspace_path}`
|
|
|
|
|
+ : "已有产物已保留;请根据 run_id 查询工作区。",
|
|
|
|
|
+ "失败严禁自动重跑、拆分任务或调用其他 subagent;必须先向用户报告真实错误和已有产物,等待用户明确指令。",
|
|
|
|
|
+ ].join("\n"),
|
|
|
metadata,
|
|
metadata,
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
@@ -343,6 +370,21 @@ async function resolveConfirmation(
|
|
|
throw new Error(`AgentPaaS 确认请求失败: HTTP ${resp.status}: ${detail.slice(0, 500)}`)
|
|
throw new Error(`AgentPaaS 确认请求失败: HTTP ${resp.status}: ${detail.slice(0, 500)}`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function cancelRemoteRun(config: { url: string; apiKey: string }, agentID: string, runID: string) {
|
|
|
|
|
+ const signal = AbortSignal.timeout(5000)
|
|
|
|
|
+ const resp = await fetch(
|
|
|
|
|
+ `${config.url}/api/v1/agents/${encodeURIComponent(agentID)}/runs/${encodeURIComponent(runID)}/cancel`,
|
|
|
|
|
+ {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: { Authorization: `Bearer ${config.apiKey}`, Accept: "application/json" },
|
|
|
|
|
+ signal,
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+ if (resp.ok || resp.status === 409) return
|
|
|
|
|
+ const detail = await resp.text().catch(() => "")
|
|
|
|
|
+ throw new Error(`AgentPaaS 取消请求失败: HTTP ${resp.status}: ${detail.slice(0, 500)}`)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<SSEEvent> {
|
|
async function* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<SSEEvent> {
|
|
|
const reader = body.getReader()
|
|
const reader = body.getReader()
|
|
|
const decoder = new TextDecoder()
|
|
const decoder = new TextDecoder()
|
|
@@ -521,7 +563,13 @@ function createResearch67Progress() {
|
|
|
const stage = stages.find((item) => item.id === stageID)
|
|
const stage = stages.find((item) => item.id === stageID)
|
|
|
if (!stage) return
|
|
if (!stage) return
|
|
|
stage.status = status
|
|
stage.status = status
|
|
|
- for (const milestone of stage.milestones) milestone.status = status
|
|
|
|
|
|
|
+ if (status === "completed") {
|
|
|
|
|
+ for (const milestone of stage.milestones) milestone.status = "completed"
|
|
|
|
|
+ } else {
|
|
|
|
|
+ for (const milestone of stage.milestones) {
|
|
|
|
|
+ if (milestone.status === "running") milestone.status = "failed"
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
current = stage
|
|
current = stage
|
|
|
active = status === "failed" ? active : undefined
|
|
active = status === "failed" ? active : undefined
|
|
|
note = status === "failed" ? "阶段执行失败" : "阶段验收通过"
|
|
note = status === "failed" ? "阶段执行失败" : "阶段验收通过"
|
|
@@ -566,7 +614,7 @@ function createResearch67Progress() {
|
|
|
},
|
|
},
|
|
|
toolResult(toolName: string, content: string) {
|
|
toolResult(toolName: string, content: string) {
|
|
|
const lower = toolName.toLowerCase()
|
|
const lower = toolName.toLowerCase()
|
|
|
- const failed = /\[error\]|mcp_error|"status"\s*:\s*"(?:failed|error)"/i.test(content)
|
|
|
|
|
|
|
+ const failed = /\[(?:tool_)?error\]|mcp_error|"status"\s*:\s*"(?:failed|error)"/i.test(content)
|
|
|
if (current?.id === "literature-review" && ["arxiv_search", "openalex_search"].includes(lower)) {
|
|
if (current?.id === "literature-review" && ["arxiv_search", "openalex_search"].includes(lower)) {
|
|
|
if (!failed) searchCalls++
|
|
if (!failed) searchCalls++
|
|
|
const milestone = current.milestones.find((item) => item.id === "search")!
|
|
const milestone = current.milestones.find((item) => item.id === "search")!
|