|
|
@@ -9,7 +9,7 @@ const DEFAULT_AGENTPAAS_URL = "http://127.0.0.1:8000"
|
|
|
type SSEEvent = { event: string; data: unknown }
|
|
|
type AgentListItem = { id: string; name: string }
|
|
|
type ConversationState = { threadID: string; runID: string }
|
|
|
-type ProgressStatus = "completed" | "running" | "pending" | "failed"
|
|
|
+type ProgressStatus = "completed" | "running" | "pending" | "failed" | "paused" | "cancelled" | "skipped" | "unknown"
|
|
|
type ProgressMilestone = {
|
|
|
id: string
|
|
|
label: string
|
|
|
@@ -34,7 +34,7 @@ export default tool({
|
|
|
- mode:iterate(多轮迭代,默认)/ chat(单轮)/ edit(单轮并指定目标子智能体)
|
|
|
- target_subagent:mode=edit 时的目标子智能体 ID
|
|
|
- work_dir:可选的服务端可见工作目录;留空时由 AgentPaaS 创建隔离 workspace
|
|
|
-- conversation:continue(默认,复用本 OpenCode session 的远端 thread/workspace)/ new(新课题)
|
|
|
+- conversation:continue(默认,沿用远端 thread 并引用上一轮成果)/ new(新课题)
|
|
|
- thread_id:可选的远端会话线程 ID;显式值优先于自动映射
|
|
|
- parameters:额外参数,原样传给远端智能体
|
|
|
|
|
|
@@ -47,6 +47,13 @@ export default tool({
|
|
|
work_dir: tool.schema.string().optional().describe("服务端可见工作目录;通常留空,由 AgentPaaS 创建"),
|
|
|
thread_id: tool.schema.string().optional().describe("远端会话线程 ID;通常留空,由 OpenCode session 自动维护"),
|
|
|
conversation: tool.schema.enum(["continue", "new"]).default("continue").describe("复用当前对话或开始新课题"),
|
|
|
+ question_id: tool.schema.number().int().min(1).max(125).optional().describe("Science 125 科学问题编号"),
|
|
|
+ feedback_kind: tool.schema.enum(["human", "program_check", "execution_result", "evidence"]).optional(),
|
|
|
+ input_artifact_ids: tool.schema
|
|
|
+ .array(tool.schema.string())
|
|
|
+ .optional()
|
|
|
+ .describe("续跑时选取的上一轮产物 ID;省略时复用上一轮已登记产物"),
|
|
|
+ idempotency_key: tool.schema.string().optional().describe("同一请求重试时复用;不同请求必须使用不同键"),
|
|
|
parameters: tool.schema
|
|
|
.record(tool.schema.string(), tool.schema.any())
|
|
|
.optional()
|
|
|
@@ -79,6 +86,7 @@ export default tool({
|
|
|
Authorization: `Bearer ${config.apiKey}`,
|
|
|
"Content-Type": "application/json",
|
|
|
Accept: "text/event-stream",
|
|
|
+ ...(args.idempotency_key ? { "Idempotency-Key": args.idempotency_key } : {}),
|
|
|
},
|
|
|
body: JSON.stringify({
|
|
|
input: args.input,
|
|
|
@@ -89,6 +97,9 @@ export default tool({
|
|
|
context: {
|
|
|
...(workDir ? { work_dir: workDir } : {}),
|
|
|
...(continueRunID ? { run_id: continueRunID } : {}),
|
|
|
+ ...(args.question_id ? { question_id: args.question_id } : {}),
|
|
|
+ ...(args.feedback_kind ? { feedback_kind: args.feedback_kind } : {}),
|
|
|
+ ...(args.input_artifact_ids ? { input_artifact_ids: args.input_artifact_ids } : {}),
|
|
|
},
|
|
|
}),
|
|
|
signal: context.abort,
|
|
|
@@ -127,174 +138,228 @@ export default tool({
|
|
|
remoteCancellation = cancelRemoteRun(config, agentID, startedRunID).catch(() => undefined)
|
|
|
}
|
|
|
context.abort.addEventListener("abort", cancelOnAbort, { once: true })
|
|
|
- for await (const ev of parseSSE(resp.body)) {
|
|
|
- const data = asRecord(ev.data)
|
|
|
- if (ev.event === "started") {
|
|
|
- startedRunID = getString(data?.run_id)
|
|
|
- const remoteThreadID = getString(data?.thread_id)
|
|
|
- if (remoteThreadID && startedRunID) {
|
|
|
- await saveConversationState(context.sessionID, conversationKey, {
|
|
|
- threadID: remoteThreadID,
|
|
|
- runID: startedRunID,
|
|
|
+ try {
|
|
|
+ for await (const ev of parseSSE(resp.body)) {
|
|
|
+ const data = asRecord(ev.data)
|
|
|
+ if (ev.event === "started") {
|
|
|
+ startedRunID = getString(data?.run_id)
|
|
|
+ const remoteThreadID = getString(data?.thread_id) || getString(data?.session_id)
|
|
|
+ if (remoteThreadID && startedRunID) {
|
|
|
+ await saveConversationState(context.sessionID, conversationKey, {
|
|
|
+ threadID: remoteThreadID,
|
|
|
+ runID: startedRunID,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (context.abort.aborted) cancelOnAbort()
|
|
|
+ updateMetadata("Research67 正在运行", {
|
|
|
+ run_id: data?.run_id,
|
|
|
+ thread_id: data?.thread_id,
|
|
|
+ latest_event: ev.event,
|
|
|
})
|
|
|
- }
|
|
|
- if (context.abort.aborted) cancelOnAbort()
|
|
|
- updateMetadata("Research67 正在运行", {
|
|
|
- run_id: data?.run_id,
|
|
|
- thread_id: data?.thread_id,
|
|
|
- latest_event: ev.event,
|
|
|
- })
|
|
|
- } else if (ev.event === "stage_started") {
|
|
|
- const stageName = getString(data?.stage_name) || getString(data?.stage_id) || "未知阶段"
|
|
|
- progress.startStage(getString(data?.stage_id), getPositiveInteger(data?.attempt))
|
|
|
- updateMetadata(`Research67 正在进行 ${stageName}`, {
|
|
|
- pipeline_id: data?.pipeline_id,
|
|
|
- stage_id: data?.stage_id,
|
|
|
- stage_name: stageName,
|
|
|
- stage_status: data?.status,
|
|
|
- attempt: data?.attempt,
|
|
|
- latest_event: ev.event,
|
|
|
- })
|
|
|
- } 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,
|
|
|
- stage_name: stageName,
|
|
|
- stage_status: data?.status,
|
|
|
- attempt: data?.attempt,
|
|
|
- latest_event: ev.event,
|
|
|
- })
|
|
|
- } 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,
|
|
|
- stage_name: stageName,
|
|
|
- stage_status: data?.status,
|
|
|
- stage_error: data?.error,
|
|
|
- attempt: data?.attempt,
|
|
|
- latest_event: ev.event,
|
|
|
- status: "failed",
|
|
|
- })
|
|
|
- } else if (ev.event === "pipeline_completed") {
|
|
|
- progress.finishPipeline()
|
|
|
- updateMetadata("Research67 流水线已完成", {
|
|
|
- pipeline_id: data?.pipeline_id,
|
|
|
- pipeline_status: data?.status,
|
|
|
- latest_event: ev.event,
|
|
|
- status: "completed",
|
|
|
- })
|
|
|
- } else if (ev.event === "confirm_required") {
|
|
|
- const runID = getString(data?.run_id) || getString(liveMetadata.run_id)
|
|
|
- const toolName = getString(data?.tool) || "高风险工具"
|
|
|
- const reason = getString(data?.reason)
|
|
|
- const input = getString(data?.input)
|
|
|
- if (!runID) throw new Error("AgentPaaS confirm_required 事件缺少 run_id")
|
|
|
+ } else if (ev.event === "stage_started") {
|
|
|
+ const stageName = getString(data?.stage_name) || getString(data?.stage_id) || "未知阶段"
|
|
|
+ progress.startStage(getString(data?.stage_id), getPositiveInteger(data?.attempt))
|
|
|
+ updateMetadata(`Research67 正在进行 ${stageName}`, {
|
|
|
+ pipeline_id: data?.pipeline_id,
|
|
|
+ stage_id: data?.stage_id,
|
|
|
+ stage_name: stageName,
|
|
|
+ stage_status: data?.status,
|
|
|
+ attempt: data?.attempt,
|
|
|
+ latest_event: ev.event,
|
|
|
+ })
|
|
|
+ } 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,
|
|
|
+ stage_name: stageName,
|
|
|
+ stage_status: data?.status,
|
|
|
+ attempt: data?.attempt,
|
|
|
+ latest_event: ev.event,
|
|
|
+ })
|
|
|
+ } 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,
|
|
|
+ stage_name: stageName,
|
|
|
+ stage_status: data?.status,
|
|
|
+ stage_error: data?.error,
|
|
|
+ attempt: data?.attempt,
|
|
|
+ latest_event: ev.event,
|
|
|
+ status: "failed",
|
|
|
+ })
|
|
|
+ } else if (ev.event === "pipeline_completed") {
|
|
|
+ progress.finishPipeline()
|
|
|
+ updateMetadata("Research67 流水线已完成", {
|
|
|
+ pipeline_id: data?.pipeline_id,
|
|
|
+ pipeline_status: data?.status,
|
|
|
+ latest_event: ev.event,
|
|
|
+ status: "completed",
|
|
|
+ })
|
|
|
+ } else if (ev.event === "confirm_required") {
|
|
|
+ const runID = getString(data?.run_id) || getString(liveMetadata.run_id)
|
|
|
+ const toolName = getString(data?.tool) || "高风险工具"
|
|
|
+ const reason = getString(data?.reason)
|
|
|
+ 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,
|
|
|
- confirmation_tool: toolName,
|
|
|
- confirmation_reason: reason,
|
|
|
- confirmation_input: input,
|
|
|
- })
|
|
|
- let approved = true
|
|
|
- try {
|
|
|
- await context.ask({
|
|
|
- permission: "agentpaas_confirm",
|
|
|
- patterns: [`${agentID}:${toolName}`],
|
|
|
- always: [`${agentID}:${toolName}`],
|
|
|
- metadata: { run_id: runID, agent_name: args.agent_name, tool: toolName, reason, input },
|
|
|
+ progress.note(`等待确认 · ${toolName}`)
|
|
|
+ updateMetadata(`Research67 正在等待确认:${toolName}`, {
|
|
|
+ latest_event: ev.event,
|
|
|
+ pending_confirmation: true,
|
|
|
+ confirmation_tool: toolName,
|
|
|
+ confirmation_reason: reason,
|
|
|
+ confirmation_input: input,
|
|
|
})
|
|
|
- } catch {
|
|
|
- approved = false
|
|
|
- }
|
|
|
- await resolveConfirmation(config, runID, approved, context.abort)
|
|
|
- progress.note(approved ? "确认已通过,继续执行" : "确认被拒绝,等待流程处理")
|
|
|
- updateMetadata(`Research67 已${approved ? "批准" : "拒绝"} ${toolName}`, {
|
|
|
- latest_event: "confirmation_resolved",
|
|
|
- pending_confirmation: false,
|
|
|
- confirmation_approved: approved,
|
|
|
- })
|
|
|
- } else if (ev.event === "tool_call") {
|
|
|
- const toolName = getString(data?.tool) || "工具"
|
|
|
- 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) || "工具"
|
|
|
- progress.toolResult(toolName, getString(data?.content))
|
|
|
- updateMetadata(withStage("Research67 已更新当前步骤", liveMetadata), {
|
|
|
- latest_event: ev.event,
|
|
|
- latest_tool: toolName,
|
|
|
- step: data?.step,
|
|
|
- })
|
|
|
- } else if (ev.event === "think" || ev.event === "think_chunk") {
|
|
|
- const now = Date.now()
|
|
|
- if (ev.event === "think" || now - lastThinkUpdate >= 1000) {
|
|
|
- lastThinkUpdate = now
|
|
|
- updateMetadata(withStage("Research67 正在思考", liveMetadata), {
|
|
|
+ let approved = true
|
|
|
+ try {
|
|
|
+ await context.ask({
|
|
|
+ permission: "agentpaas_confirm",
|
|
|
+ patterns: [`${agentID}:${toolName}`],
|
|
|
+ always: [`${agentID}:${toolName}`],
|
|
|
+ metadata: { run_id: runID, agent_name: args.agent_name, tool: toolName, reason, input },
|
|
|
+ })
|
|
|
+ } catch {
|
|
|
+ approved = false
|
|
|
+ }
|
|
|
+ await resolveConfirmation(config, agentID, runID, approved, context.abort, getString(data?.approval_id))
|
|
|
+ progress.note(approved ? "确认已通过,继续执行" : "确认被拒绝,等待流程处理")
|
|
|
+ updateMetadata(`Research67 已${approved ? "批准" : "拒绝"} ${toolName}`, {
|
|
|
+ latest_event: "confirmation_resolved",
|
|
|
+ pending_confirmation: false,
|
|
|
+ confirmation_approved: approved,
|
|
|
+ })
|
|
|
+ } else if (ev.event === "tool_call") {
|
|
|
+ const toolName = getString(data?.tool) || "工具"
|
|
|
+ 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) || "工具"
|
|
|
+ progress.toolResult(toolName, getString(data?.content))
|
|
|
+ updateMetadata(withStage("Research67 已更新当前步骤", liveMetadata), {
|
|
|
+ latest_event: ev.event,
|
|
|
+ latest_tool: toolName,
|
|
|
+ step: data?.step,
|
|
|
+ })
|
|
|
+ } else if (ev.event === "think" || ev.event === "think_chunk") {
|
|
|
+ const now = Date.now()
|
|
|
+ if (ev.event === "think" || now - lastThinkUpdate >= 1000) {
|
|
|
+ lastThinkUpdate = now
|
|
|
+ updateMetadata(withStage("Research67 正在思考", liveMetadata), {
|
|
|
+ latest_event: ev.event,
|
|
|
+ step: data?.step,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else if (ev.event === "answer") {
|
|
|
+ updateMetadata(withStage("Research67 正在整理结果", liveMetadata), {
|
|
|
latest_event: ev.event,
|
|
|
step: data?.step,
|
|
|
})
|
|
|
+ } else if (ev.event === "heartbeat") {
|
|
|
+ updateMetadata(withStage("Research67 仍在运行", liveMetadata), { latest_event: ev.event })
|
|
|
+ } 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 === "paused") {
|
|
|
+ progress.pause()
|
|
|
+ updateMetadata("Research67 已暂停", {
|
|
|
+ latest_event: ev.event,
|
|
|
+ status: "paused",
|
|
|
+ checkpoint_id: data?.checkpoint_id,
|
|
|
+ workspace_path: data?.workspace_path ?? liveMetadata.workspace_path,
|
|
|
+ })
|
|
|
+ } else if (ev.event === "approval_required") {
|
|
|
+ // Durable human gates stop the stream. Surface the approval card; a separate
|
|
|
+ // control action must approve it and re-subscribe to the same run.
|
|
|
+ updateMetadata("Research67 等待人工反馈", {
|
|
|
+ latest_event: ev.event,
|
|
|
+ status: "waiting_human",
|
|
|
+ pending_approval: 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
|
|
|
+ break
|
|
|
}
|
|
|
- } else if (ev.event === "answer") {
|
|
|
- updateMetadata(withStage("Research67 正在整理结果", liveMetadata), {
|
|
|
- latest_event: ev.event,
|
|
|
- step: data?.step,
|
|
|
- })
|
|
|
- } else if (ev.event === "heartbeat") {
|
|
|
- updateMetadata(withStage("Research67 仍在运行", liveMetadata), { latest_event: ev.event })
|
|
|
- } 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
|
|
|
- progress.cancel()
|
|
|
- updateMetadata("Research67 已取消", { latest_event: ev.event, status: "cancelled" })
|
|
|
- } else if (ev.event === "done" && data) {
|
|
|
- done = data
|
|
|
- break
|
|
|
}
|
|
|
+ } finally {
|
|
|
+ context.abort.removeEventListener("abort", cancelOnAbort)
|
|
|
}
|
|
|
- context.abort.removeEventListener("abort", cancelOnAbort)
|
|
|
if (!done) {
|
|
|
const hint = sawCancelled ? "(收到 cancelled 事件)" : "(未收到 done 事件,连接可能中断)"
|
|
|
throw new Error(`AgentPaaS 任务未正常结束${hint}${errors.length ? ":" + errors.join("; ") : ""}`)
|
|
|
}
|
|
|
const status = typeof done.status === "string" ? done.status : "unknown"
|
|
|
+ if (status === "completed" || status === "failed") progress.stopUnverified()
|
|
|
+ if (status === "paused") progress.pause()
|
|
|
+ if (status === "cancelled") progress.cancel()
|
|
|
const progressMetadata = progress.snapshot()
|
|
|
const metadata = {
|
|
|
agent_name: args.agent_name,
|
|
|
agent_id: agentID,
|
|
|
run_id: done.run_id ?? liveMetadata.run_id,
|
|
|
- thread_id: done.thread_id ?? liveMetadata.thread_id,
|
|
|
+ thread_id: done.thread_id ?? done.session_id ?? liveMetadata.thread_id,
|
|
|
status,
|
|
|
steps: done.steps,
|
|
|
- total_tokens: done.total_tokens,
|
|
|
- cost_usd: done.cost_usd,
|
|
|
+ total_tokens: done.total_tokens ?? asRecord(done.usage)?.total_tokens,
|
|
|
+ cost_usd: done.cost_usd ?? asRecord(done.usage)?.cost_usd,
|
|
|
+ research: done.research,
|
|
|
+ artifacts: Array.isArray(done.artifacts) ? done.artifacts : [],
|
|
|
workspace_path: done.workspace_path ?? liveMetadata.workspace_path,
|
|
|
error_code: liveMetadata.error_code,
|
|
|
error_message: liveMetadata.error_message,
|
|
|
+ checkpoint_id: done.checkpoint_id ?? liveMetadata.checkpoint_id,
|
|
|
+ pending_approval: done.pending_approval ?? liveMetadata.pending_approval,
|
|
|
...(progressMetadata ? { research67_progress: progressMetadata } : {}),
|
|
|
}
|
|
|
+ const artifactLinks = metadata.artifacts.flatMap((value) => {
|
|
|
+ const artifact = asRecord(value)
|
|
|
+ const id = getString(artifact?.artifact_id)
|
|
|
+ if (!id) return []
|
|
|
+ const name = (getString(artifact?.name) || id).replace(/[\[\]\r\n]/g, "_")
|
|
|
+ return [
|
|
|
+ `- [${name}](${config.url}/api/v1/agents/${encodeURIComponent(agentID)}/runs/${encodeURIComponent(String(metadata.run_id))}/artifacts/${encodeURIComponent(id)}/download)`,
|
|
|
+ ]
|
|
|
+ })
|
|
|
if (status === "completed")
|
|
|
- return { title: `AgentPaaS ${args.agent_name}`, output: String(done.output ?? ""), metadata }
|
|
|
+ return {
|
|
|
+ title: `AgentPaaS ${args.agent_name}`,
|
|
|
+ output: [
|
|
|
+ String(done.output ?? ""),
|
|
|
+ ...(artifactLinks.length ? ["\n研究产物(通过 PaaS 身份验证后下载):", ...artifactLinks] : []),
|
|
|
+ ].join("\n"),
|
|
|
+ metadata,
|
|
|
+ }
|
|
|
if (status === "cancelled") return { title: `AgentPaaS ${args.agent_name}`, output: "任务已被取消。", metadata }
|
|
|
+ if (status === "paused")
|
|
|
+ return {
|
|
|
+ title: `AgentPaaS ${args.agent_name}(已暂停)`,
|
|
|
+ output: `任务已暂停。run_id=${String(metadata.run_id ?? "")};checkpoint_id=${String(metadata.checkpoint_id ?? "未提供")}。恢复应使用该 checkpoint,不能重新提交任务。`,
|
|
|
+ metadata,
|
|
|
+ }
|
|
|
+ if (status === "waiting_human" || status === "waiting_confirmation")
|
|
|
+ return {
|
|
|
+ title: `AgentPaaS ${args.agent_name}(等待确认)`,
|
|
|
+ output: `任务正在等待人工确认。run_id=${String(metadata.run_id ?? "")}。请检查待确认材料并提交决定;这不是任务失败。`,
|
|
|
+ metadata,
|
|
|
+ }
|
|
|
return {
|
|
|
title: `AgentPaaS ${args.agent_name}(失败)`,
|
|
|
output: [
|
|
|
@@ -340,10 +405,28 @@ async function resolveAgentID(config: { url: string; apiKey: string }, agentName
|
|
|
if (matches.length === 0) throw new Error(`AgentPaaS 中没有名为“${name}”的 active Agent`)
|
|
|
if (matches.length > 1)
|
|
|
throw new Error(`AgentPaaS 中存在 ${matches.length} 个名为“${name}”的 active Agent,无法确定调用目标`)
|
|
|
- if (!matches[0].id.startsWith("ag_")) throw new Error(`AgentPaaS 为“${name}”返回了无效 ID: ${matches[0].id}`)
|
|
|
+ if (!isSupportedAgentID(matches[0].id)) throw new Error(`AgentPaaS 为“${name}”返回了无效 ID: ${matches[0].id}`)
|
|
|
return matches[0].id
|
|
|
}
|
|
|
|
|
|
+// Parse the two supported ID namespaces; never accept URL/path syntax or decoded input.
|
|
|
+// Every route still encodes the complete ID as one path segment.
|
|
|
+function isSupportedAgentID(id: string): boolean {
|
|
|
+ if (id.length > 512) return false
|
|
|
+ if (id.startsWith("ag_")) return /^ag_[A-Za-z0-9_-]+$/.test(id)
|
|
|
+ const parts = id.split(":")
|
|
|
+ if (parts.length !== 3 || parts[0] !== "locostack") return false
|
|
|
+ const namespace = parts[1]
|
|
|
+ const name = parts[2]
|
|
|
+ const label = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
|
|
|
+ return (
|
|
|
+ namespace.length <= 63 &&
|
|
|
+ label.test(namespace) &&
|
|
|
+ name.length <= 253 &&
|
|
|
+ name.split(".").every((part) => part.length <= 63 && label.test(part))
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
const conversationCache = new Map<string, ConversationState>()
|
|
|
|
|
|
async function loadConversationState(sessionID: string, key: string): Promise<ConversationState | undefined> {
|
|
|
@@ -432,22 +515,32 @@ function agentPaaSConfigPath() {
|
|
|
}
|
|
|
|
|
|
function agentPaaSConversationDir() {
|
|
|
- return path.join(agentPaaSHome(), ".agentpaas", "opencode-conversations")
|
|
|
+ return (
|
|
|
+ process.env.AGENTPAAS_BRIDGE_STATE_DIR?.trim() || path.join(agentPaaSHome(), ".agentpaas", "opencode-conversations")
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
async function resolveConfirmation(
|
|
|
config: { url: string; apiKey: string },
|
|
|
+ agentID: string,
|
|
|
runID: string,
|
|
|
approved: boolean,
|
|
|
signal: AbortSignal,
|
|
|
+ approvalID: string,
|
|
|
) {
|
|
|
- const resp = await fetch(`${config.url}/api/v1/traces/${encodeURIComponent(runID)}/confirm`, {
|
|
|
+ // Compatibility adapter: legacy local runs keep their existing confirmation API.
|
|
|
+ // Remote runs must use their owner-aware route; never fall back after a failed POST.
|
|
|
+ const remote = agentID.startsWith("locostack:")
|
|
|
+ const endpoint = remote
|
|
|
+ ? `/api/v1/agents/${encodeURIComponent(agentID)}/runs/${encodeURIComponent(runID)}/confirm`
|
|
|
+ : `/api/v1/traces/${encodeURIComponent(runID)}/confirm`
|
|
|
+ const resp = await fetch(`${config.url}${endpoint}`, {
|
|
|
method: "POST",
|
|
|
headers: {
|
|
|
Authorization: `Bearer ${config.apiKey}`,
|
|
|
"Content-Type": "application/json",
|
|
|
},
|
|
|
- body: JSON.stringify({ approved }),
|
|
|
+ body: JSON.stringify({ approved, ...(remote && approvalID ? { approval_id: approvalID } : {}) }),
|
|
|
signal,
|
|
|
})
|
|
|
if (resp.ok) return
|
|
|
@@ -465,7 +558,7 @@ async function cancelRemoteRun(config: { url: string; apiKey: string }, agentID:
|
|
|
signal,
|
|
|
},
|
|
|
)
|
|
|
- if (resp.ok || resp.status === 409) return
|
|
|
+ if (resp.ok) return
|
|
|
const detail = await resp.text().catch(() => "")
|
|
|
throw new Error(`AgentPaaS 取消请求失败: HTTP ${resp.status}: ${detail.slice(0, 500)}`)
|
|
|
}
|
|
|
@@ -474,31 +567,36 @@ async function* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<SSEEv
|
|
|
const reader = body.getReader()
|
|
|
const decoder = new TextDecoder()
|
|
|
let buffer = ""
|
|
|
- while (true) {
|
|
|
- const { done, value } = await reader.read()
|
|
|
- if (done) break
|
|
|
- buffer += decoder.decode(value, { stream: true })
|
|
|
- buffer = buffer.replace(/\r\n/g, "\n")
|
|
|
- let idx: number
|
|
|
- while ((idx = buffer.indexOf("\n\n")) >= 0) {
|
|
|
- const chunk = buffer.slice(0, idx)
|
|
|
- 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 {
|
|
|
- data = JSON.parse(dataRaw)
|
|
|
- } catch {
|
|
|
- /* 非 JSON 当纯文本 */
|
|
|
+ try {
|
|
|
+ while (true) {
|
|
|
+ const { done, value } = await reader.read()
|
|
|
+ if (done) break
|
|
|
+ buffer += decoder.decode(value, { stream: true })
|
|
|
+ buffer = buffer.replace(/\r\n/g, "\n")
|
|
|
+ let idx: number
|
|
|
+ while ((idx = buffer.indexOf("\n\n")) >= 0) {
|
|
|
+ const chunk = buffer.slice(0, idx)
|
|
|
+ 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 {
|
|
|
+ data = JSON.parse(dataRaw)
|
|
|
+ } catch {
|
|
|
+ /* 非 JSON 当纯文本 */
|
|
|
+ }
|
|
|
}
|
|
|
+ if (event) yield { event, data }
|
|
|
}
|
|
|
- if (event) yield { event, data }
|
|
|
}
|
|
|
+ } finally {
|
|
|
+ await reader.cancel().catch(() => undefined)
|
|
|
+ reader.releaseLock()
|
|
|
}
|
|
|
}
|
|
|
function parseSSEField(chunk: string, field: string): string {
|
|
|
@@ -700,6 +798,7 @@ function createResearch67Progress() {
|
|
|
],
|
|
|
},
|
|
|
]
|
|
|
+ const observedStages: ProgressStage[] = []
|
|
|
let current: ProgressStage | undefined
|
|
|
let active: ProgressMilestone | undefined
|
|
|
const pendingArtifacts = new Map<string, Array<Array<{ milestone: ProgressMilestone; key: string }>>>()
|
|
|
@@ -750,7 +849,10 @@ function createResearch67Progress() {
|
|
|
const findOrCreateStage = (stageID: string) => {
|
|
|
const normalized = normalizeStageID(stageID || "remote-stage")
|
|
|
let stage = stages.find((item) => item.id === normalized)
|
|
|
- if (stage) return stage
|
|
|
+ if (stage) {
|
|
|
+ if (!observedStages.includes(stage)) observedStages.push(stage)
|
|
|
+ return stage
|
|
|
+ }
|
|
|
stage = {
|
|
|
id: normalized,
|
|
|
label: `${stageID || "remote-stage"} · 远端阶段`,
|
|
|
@@ -758,6 +860,7 @@ function createResearch67Progress() {
|
|
|
milestones: genericMilestones(),
|
|
|
}
|
|
|
stages.push(stage)
|
|
|
+ observedStages.push(stage)
|
|
|
return stage
|
|
|
}
|
|
|
|
|
|
@@ -853,7 +956,7 @@ function createResearch67Progress() {
|
|
|
|
|
|
const completeArtifact = (item: { milestone: ProgressMilestone; key: string }) => {
|
|
|
item.milestone.done?.add(item.key)
|
|
|
- if (!item.milestone.total || item.milestone.done?.size < item.milestone.total) return
|
|
|
+ if (!item.milestone.total || (item.milestone.done?.size ?? 0) < item.milestone.total) return
|
|
|
item.milestone.status = "completed"
|
|
|
if (active !== item.milestone || !current) return
|
|
|
const next = current.milestones[current.milestones.indexOf(item.milestone) + 1]
|
|
|
@@ -884,10 +987,7 @@ function createResearch67Progress() {
|
|
|
if (stage === current) resetStage(stage)
|
|
|
current = stage
|
|
|
activeAttempt = attempt
|
|
|
- for (const item of stages) {
|
|
|
- if (item === current) break
|
|
|
- if (item.status === "pending") item.status = "completed"
|
|
|
- }
|
|
|
+ // A later stage starting says nothing about whether earlier stages ran.
|
|
|
current.status = "running"
|
|
|
startMilestone(current.milestones[0])
|
|
|
},
|
|
|
@@ -907,17 +1007,36 @@ function createResearch67Progress() {
|
|
|
note = status === "failed" ? "阶段执行失败" : "阶段验收通过"
|
|
|
},
|
|
|
finishPipeline() {
|
|
|
- for (const stage of stages) {
|
|
|
- stage.status = "completed"
|
|
|
- for (const milestone of stage.milestones) milestone.status = "completed"
|
|
|
+ // Terminal pipeline status is not evidence that every predefined stage ran.
|
|
|
+ for (const stage of observedStages) {
|
|
|
+ if (stage.status === "running") stage.status = "unknown"
|
|
|
+ for (const milestone of stage.milestones) {
|
|
|
+ if (milestone.status === "running") milestone.status = "unknown"
|
|
|
+ }
|
|
|
}
|
|
|
active = undefined
|
|
|
timerRunning = false
|
|
|
- note = "全部阶段已完成"
|
|
|
+ note = "流程已结束;阶段状态以服务端事件为准"
|
|
|
+ },
|
|
|
+ stopUnverified() {
|
|
|
+ for (const stage of observedStages) {
|
|
|
+ if (stage.status === "running") stage.status = "unknown"
|
|
|
+ for (const milestone of stage.milestones) {
|
|
|
+ if (milestone.status === "running") milestone.status = "unknown"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ active = undefined
|
|
|
+ timerRunning = false
|
|
|
+ },
|
|
|
+ pause() {
|
|
|
+ if (current?.status === "running") current.status = "paused"
|
|
|
+ if (active?.status === "running") active.status = "paused"
|
|
|
+ timerRunning = false
|
|
|
+ note = "任务已暂停,等待恢复"
|
|
|
},
|
|
|
cancel() {
|
|
|
- if (current?.status === "running") current.status = "failed"
|
|
|
- if (active?.status === "running") active.status = "failed"
|
|
|
+ if (current?.status === "running") current.status = "cancelled"
|
|
|
+ if (active?.status === "running") active.status = "cancelled"
|
|
|
timerRunning = false
|
|
|
note = "任务已取消"
|
|
|
},
|
|
|
@@ -1009,7 +1128,7 @@ function createResearch67Progress() {
|
|
|
},
|
|
|
snapshot() {
|
|
|
if (!current) return undefined
|
|
|
- const rows = stages.flatMap((stage) => {
|
|
|
+ const rows = observedStages.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
|