feat(mcp): resume queued calculation jobs
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { TaskCalculationWaiter } from "../mcp/task-calculation-waiter.js";
|
||||
import { registerInteractionRequestRoutes } from "./interaction-request-routes.js";
|
||||
import type { InteractionRequestStore } from "../sessions/interaction-request-store.js";
|
||||
import { Router, type NextFunction, type Request, type Response } from "express";
|
||||
@@ -33,6 +34,7 @@ import { registerArtifactRoutes } from "./artifact-routes.js";
|
||||
import type { AgentMessageCoordinator } from "../chat/agent-message-coordinator.js";
|
||||
|
||||
export type BuildChatRouterOptions = {
|
||||
waitTaskCalculations?: TaskCalculationWaiter;
|
||||
interactionRequestStore?: InteractionRequestStore;
|
||||
agentMessages?: AgentMessageCoordinator;
|
||||
frontendActionCoordinator: FrontendActionCoordinator;
|
||||
@@ -170,6 +172,7 @@ export const buildChatRouter = (options: BuildChatRouterOptions) => {
|
||||
|
||||
registerResponseInputRoutes(chatRouter, { runtime, sessionMetadataStore, runState });
|
||||
registerChatStreamRoute(chatRouter, {
|
||||
waitTaskCalculations: options.waitTaskCalculations,
|
||||
interactionRequestStore: options.interactionRequestStore,
|
||||
agentMessages,
|
||||
frontendActionCoordinator,
|
||||
|
||||
@@ -77,7 +77,7 @@ type StreamPromptOptions = {
|
||||
onInboxEvent?: (type: string, data: { inboxID?: string; delivery?: "steer" | "queue" }) => void;
|
||||
waitForUserInput?: () => Promise<boolean>;
|
||||
beforePrompt?: () => Promise<void>;
|
||||
beforeIdle?: () => Promise<boolean>;
|
||||
beforeIdle?: () => Promise<boolean | string>;
|
||||
beforeRichAnswerClose?: () => Promise<boolean>;
|
||||
runtime: OpencodeRuntimeAdapter;
|
||||
sessionId: string;
|
||||
@@ -904,7 +904,7 @@ export const streamPromptResponse = async ({
|
||||
id: part.id,
|
||||
phase: "tool",
|
||||
status: normalizeToolStatus(part.state.status),
|
||||
title: getToolProgressTitle(part.tool, part.state.status),
|
||||
title: getToolProgressTitle(part.tool, part.state.status, part.state.status === "completed" ? part.state.output : undefined),
|
||||
tool: { name: part.tool, call_id: part.callID, input: sanitizeToolProgressInput(toolParams) },
|
||||
detail: buildToolProgressDetail(
|
||||
part.tool,
|
||||
@@ -912,6 +912,7 @@ export const streamPromptResponse = async ({
|
||||
toolParams,
|
||||
reason,
|
||||
part.state.status === "error" ? part.state.error : undefined,
|
||||
part.state.status === "completed" ? part.state.output : undefined,
|
||||
),
|
||||
});
|
||||
if (
|
||||
@@ -1022,12 +1023,13 @@ export const streamPromptResponse = async ({
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (beforeIdle && !await beforeIdle()) {
|
||||
const idleDecision = beforeIdle ? await beforeIdle() : true;
|
||||
if (idleDecision === false || typeof idleDecision === "string") {
|
||||
// A durable message won the race with completion. Continue within
|
||||
// the same managed session/lease, never create another task run.
|
||||
finalAnswer = undefined;
|
||||
sawResponseActivity = false;
|
||||
await runtime.prompt(sessionId, "请处理收信箱中新到达的调查协作消息,按需回复,然后完成本轮工作。", toRuntimeModel(model));
|
||||
await runtime.prompt(sessionId, typeof idleDecision === "string" ? idleDecision : "请处理收信箱中新到达的调查协作消息,按需回复,然后完成本轮工作。", toRuntimeModel(model));
|
||||
continue;
|
||||
}
|
||||
if (waitForUserInput && await waitForUserInput()) {
|
||||
|
||||
@@ -468,6 +468,7 @@ export const buildToolProgressDetail = (
|
||||
params: Record<string, unknown>,
|
||||
reason: string,
|
||||
error?: string,
|
||||
output?: string,
|
||||
) => {
|
||||
const toolName = toolLabels[tool] ?? tool;
|
||||
const reasonText = reason ? `;说明:${containsForbiddenPersistentContent(reason) ? "[redacted]" : reason}` : "";
|
||||
@@ -478,6 +479,8 @@ export const buildToolProgressDetail = (
|
||||
return `${toolName} 调用失败${reasonText}${paramsText}${errorText}`;
|
||||
}
|
||||
if (status === "completed") {
|
||||
const calculation = calculationProgress(output);
|
||||
if (calculation) return `${toolName}:${calculation.label};计算任务:${calculation.jobId}${reasonText}${paramsText}`;
|
||||
return `${toolName} 已执行完成${reasonText}${paramsText}`;
|
||||
}
|
||||
if (status === "pending") {
|
||||
@@ -486,8 +489,28 @@ export const buildToolProgressDetail = (
|
||||
return `${toolName} 正在执行${reasonText}${paramsText}`;
|
||||
};
|
||||
|
||||
export const getToolProgressTitle = (tool: string, status: string) => {
|
||||
const calculationProgress = (output?: string) => {
|
||||
if (!output || output.length > 100_000) return null;
|
||||
try {
|
||||
const value = JSON.parse(output);
|
||||
if (value?.semantics !== "task" || typeof value.data?.job_id !== "string" ||
|
||||
!/^[0-9a-f-]{36}$/i.test(value.data.job_id)) return null;
|
||||
const labels: Record<string, string> = {
|
||||
queued: "已提交,等待计算", running: "后台计算中", cancel_requested: "已请求取消,等待计算停止",
|
||||
succeeded: "计算已完成", failed: "计算失败", cancelled: "计算已取消",
|
||||
interrupted: "计算已中断", timed_out: "计算执行超时",
|
||||
};
|
||||
const label = labels[value.data.status];
|
||||
return label ? { label, jobId: value.data.job_id } : null;
|
||||
} catch { return null; }
|
||||
};
|
||||
|
||||
export const getToolProgressTitle = (tool: string, status: string, output?: string) => {
|
||||
const toolName = toolLabels[tool] ?? tool;
|
||||
if (status === "completed") {
|
||||
const calculation = calculationProgress(output);
|
||||
if (calculation) return `${toolName}:${calculation.label}`;
|
||||
}
|
||||
if (status === "completed") return `${toolName} 已完成`;
|
||||
if (status === "error") return `${toolName} 执行失败`;
|
||||
if (status === "pending") return `准备调用 ${toolName}`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { TaskCalculationWaiter } from "../mcp/task-calculation-waiter.js";
|
||||
import { createResponsesProjection, responseRequestSchema, responsePrompt } from "../chat/responses-protocol.js";
|
||||
import { ResponseInputTracker, WaitingResponseInputQueue } from "../chat/response-inputs.js";
|
||||
import { reconcileInteractionDelivery } from "../sessions/interaction-delivery-reconciliation.js";
|
||||
@@ -252,6 +253,7 @@ export const buildFailedTaskExecutionPlan = (history: Record<string, unknown>[])
|
||||
};
|
||||
|
||||
type RegisterChatStreamRouteOptions = {
|
||||
waitTaskCalculations?: TaskCalculationWaiter;
|
||||
interactionRequestStore?: InteractionRequestStore;
|
||||
agentMessages?: AgentMessageCoordinator;
|
||||
frontendActionCoordinator: FrontendActionCoordinator;
|
||||
@@ -269,6 +271,7 @@ export const registerChatStreamRoute = (
|
||||
chatRouter: Router,
|
||||
{
|
||||
interactionRequestStore,
|
||||
waitTaskCalculations,
|
||||
frontendActionCoordinator,
|
||||
learningOrchestrator,
|
||||
memoryStore,
|
||||
@@ -1005,9 +1008,11 @@ export const registerChatStreamRoute = (
|
||||
if (messaging && (messaging.hasInputThisTurn(binding.sessionId) || !await messaging.closeInteractive(activeSessionRecord))) return false;
|
||||
return inputTracker.canClose();
|
||||
},
|
||||
beforeIdle: messaging ? async () => {
|
||||
if (sessionPurpose !== "task_execution") return messaging.closeInteractive(activeSessionRecord);
|
||||
if (await messaging.flush(binding.sessionId)) return false;
|
||||
beforeIdle: messaging || (sessionPurpose === "task_execution" && waitTaskCalculations) ? async () => {
|
||||
if (sessionPurpose !== "task_execution") return messaging ? messaging.closeInteractive(activeSessionRecord) : true;
|
||||
if (await messaging?.flush(binding.sessionId)) return false;
|
||||
const calculationResume = await waitTaskCalculations?.(binding.sessionId, abortController.signal);
|
||||
if (calculationResume) return calculationResume;
|
||||
await taskArtifactPublishQueue;
|
||||
if (taskLeaseFailure || !activeSessionRecord.taskId || !activeSessionRecord.runId) return true;
|
||||
taskCompleting = true;
|
||||
@@ -1018,13 +1023,13 @@ export const registerChatStreamRoute = (
|
||||
const task = await operationalTaskClient.completeTaskRun(binding.sessionId,
|
||||
activeSessionRecord.taskId, activeSessionRecord.runId, clientSessionId);
|
||||
taskRunFinalized = true;
|
||||
messaging.stop(binding.sessionId);
|
||||
messaging?.stop(binding.sessionId);
|
||||
await stopTaskHeartbeat();
|
||||
if (task.lifecycle_status === "failed") throw new Error("Server rejected incomplete investigation artifacts");
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "PENDING_AGENT_MESSAGES") {
|
||||
await messaging.flush(binding.sessionId);
|
||||
await messaging?.flush(binding.sessionId);
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
@@ -1048,7 +1053,6 @@ export const registerChatStreamRoute = (
|
||||
publish("execution_plan", buildFailedTaskExecutionPlan(executionPlanHistory));
|
||||
}
|
||||
await taskArtifactPublishQueue;
|
||||
await stopTaskHeartbeat();
|
||||
if (
|
||||
sessionPurpose === "task_execution"
|
||||
&& activeSessionRecord.taskId
|
||||
@@ -1068,6 +1072,7 @@ export const registerChatStreamRoute = (
|
||||
taskRunFinalized = true;
|
||||
} else {
|
||||
try {
|
||||
if (await waitTaskCalculations?.(binding.sessionId, abortController.signal)) throw Object.assign(new Error("Calculation results require investigation continuation"), { code: "CALCULATION_CONTINUATION_REQUIRED" });
|
||||
const completedTask = await operationalTaskClient.completeTaskRun(
|
||||
binding.sessionId,
|
||||
activeSessionRecord.taskId,
|
||||
@@ -1096,6 +1101,7 @@ export const registerChatStreamRoute = (
|
||||
}
|
||||
}
|
||||
}
|
||||
await stopTaskHeartbeat();
|
||||
await persistQueue.catch((error) => {
|
||||
logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist chat stream state");
|
||||
});
|
||||
@@ -1153,6 +1159,7 @@ export const registerChatStreamRoute = (
|
||||
session_id: clientSessionId,
|
||||
title: sessionTitle,
|
||||
});
|
||||
await stopTaskHeartbeat();
|
||||
await persistQueue.catch((error) => {
|
||||
logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist chat stream state");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user