feat(agent): unify MCP tools and adopt Responses delivery

Move project and host queries into native MCP with host-owned OAuth/context,
while retaining memory, skills, actions and publication plugins. Add nearby-model
queries and explicit chat/task/learning model defaults.

Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped
session ownership, pending input edits and investigation communication.

Disable local-provider discovery: Server /health allowed native vLLM to probe
/v1/models on port 8000. A native before/after regression prevents recurrence.

Validation: 170 focused Bun tests; service/plugin typechecks; native discovery,
provider-discovery and input-inbox smoke checks; no paid model inference.
This commit is contained in:
2026-10-19 18:46:58 +08:00
parent f5c3cde2c2
commit 6a49d2209e
164 changed files with 12286 additions and 2368 deletions
+47 -9
View File
@@ -70,6 +70,12 @@ export {
export type ApprovalMode = "request" | "always";
type StreamPromptOptions = {
onPromptDispatched?: () => void;
onInboxEvent?: (type: string, data: { inboxID?: string; delivery?: "steer" | "queue" }) => void;
waitForUserInput?: () => Promise<boolean>;
beforePrompt?: () => Promise<void>;
beforeIdle?: () => Promise<boolean>;
beforeRichAnswerClose?: () => Promise<boolean>;
runtime: OpencodeRuntimeAdapter;
sessionId: string;
clientSessionId: string;
@@ -146,6 +152,12 @@ export const streamPromptResponse = async ({
suppressLegacyFrontendActions = false,
suppressAnswerUi = false,
write,
beforePrompt,
beforeIdle,
beforeRichAnswerClose,
onPromptDispatched,
onInboxEvent,
waitForUserInput,
}: StreamPromptOptions): Promise<{
aborted: boolean;
failed: boolean;
@@ -264,8 +276,11 @@ export const streamPromptResponse = async ({
detail: "已接收用户消息,正在建立会话并准备进入分析、规划和工具调用阶段。",
});
const promptPromise = runtime
.prompt(sessionId, message, toRuntimeModel(model))
const promptPromise = Promise.resolve().then(async () => {
await beforePrompt?.();
await runtime.prompt(sessionId, message, toRuntimeModel(model));
onPromptDispatched?.();
})
.then(() => {
promptSettled = true;
logDevelopmentDebug("runtime.prompt resolved", {
@@ -337,6 +352,11 @@ export const streamPromptResponse = async ({
if (!isSessionEvent(event, sessionId)) {
continue;
}
const inboxEvent = event as unknown as { type: string; properties: { inboxID?: string; delivery?: "steer" | "queue" } };
if (inboxEvent.type.startsWith("session.inbox.")) {
onInboxEvent?.(inboxEvent.type, inboxEvent.properties);
continue;
}
if (!firstSessionEventLogged) {
firstSessionEventLogged = true;
@@ -410,19 +430,21 @@ export const streamPromptResponse = async ({
title: autoApprove ? "已自动允许权限请求" : "等待权限确认",
detail:
autoApprove
? "当前批准模式为始终允许,已自动允许本次权限请求。"
? "当前模式为本轮自动批准,已允许本次请求,不保存后续轮次授权。"
: buildPermissionDetail(event),
});
if (autoApprove) {
await runtime.replyPermission({
requestId: event.properties.id,
sessionId,
reply: "always",
// Automatic approval applies only while this round is active.
// Native "always" would also authorize later manual rounds.
reply: "once",
});
write("permission_response", {
session_id: clientSessionId,
request_id: event.properties.id,
reply: "always" satisfies PermissionReply,
reply: "once" satisfies PermissionReply,
});
continue;
}
@@ -456,19 +478,19 @@ export const streamPromptResponse = async ({
title: autoApprove ? "已自动允许权限请求" : "等待权限确认",
detail:
autoApprove
? "当前批准模式为始终允许,已自动允许本次权限请求。"
? "当前模式为本轮自动批准,已允许本次请求,不保存后续轮次授权。"
: buildPermissionV2Detail(event),
});
if (autoApprove) {
await runtime.replyPermission({
requestId: event.properties.id,
sessionId,
reply: "always",
reply: "once",
});
write("permission_response", {
session_id: clientSessionId,
request_id: event.properties.id,
reply: "always" satisfies PermissionReply,
reply: "once" satisfies PermissionReply,
});
continue;
}
@@ -750,7 +772,8 @@ export const streamPromptResponse = async ({
const refs = (output as { ui_block_ids?: unknown }).ui_block_ids;
finalAnswerUiBlockIds = Array.isArray(refs) ? [...new Set(refs.filter((id): id is string => typeof id === "string" && presentedAnswerIds.has(id)))].slice(0, 48) : [];
const hasPendingTool = [...toolStatuses.values()].some((status) => status !== "completed" && status !== "error");
if (presentedAnswerIds.size > 0 && !suppressAnswerUi && !hasPendingTool) {
if (presentedAnswerIds.size > 0 && !suppressAnswerUi && !hasPendingTool
&& (!beforeRichAnswerClose || await beforeRichAnswerClose())) {
// A final rich-chat answer closes this turn; native interruption stops
// automatic revisions without marking a background task complete.
await runtime.abortSession(sessionId);
@@ -966,6 +989,21 @@ export const streamPromptResponse = async ({
});
continue;
}
if (beforeIdle && !await beforeIdle()) {
// 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));
continue;
}
if (waitForUserInput && await waitForUserInput()) {
// Native inbox admission already resumes queued work; do not inject a
// synthetic extra prompt or close the stream between executions.
finalAnswer = undefined;
sawResponseActivity = false;
continue;
}
logDevelopmentDebug("session idle received", {
...debugContext,
emittedText,