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
+9 -36
View File
@@ -4,12 +4,9 @@ import { z } from "zod";
import { getLocalAgentContext } from "../context/localContext.js";
import { type ChatSessionBridge } from "../chat/sessionBridge.js";
import { logger } from "../logger.js";
import { type ResultReferenceResolver } from "../results/resolver.js";
import { RESULT_REFERENCE_KIND } from "../results/store.js";
import { type SessionMetadataStore } from "../sessions/metadataStore.js";
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
import { type FrontendActionCoordinator } from "../frontendAction/coordinator.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
import {
type ActiveRun,
type RunStatus,
@@ -25,15 +22,16 @@ const abortPayloadSchema = z.object({
type RegisterAuxiliaryRoutesOptions = {
activeRuns: Map<string, ActiveRun>;
lastRunStatuses: Map<string, RunStatus>;
resultReferenceResolver: ResultReferenceResolver;
sessionBridge: ChatSessionBridge;
sessionMetadataStore: SessionMetadataStore;
sessionUiStateStore: SessionUiStateStore;
frontendActionCoordinator: FrontendActionCoordinator;
};
const toSessionUiStateContext = (sessionId: string) => ({
sessionId,
const toSessionUiStateContext = (record: { sessionId: string; actorKey: string; projectKey: string }) => ({
sessionId: record.sessionId,
actorKey: record.actorKey,
projectKey: record.projectKey,
});
export const registerChatAuxiliaryRoutes = (
@@ -41,38 +39,12 @@ export const registerChatAuxiliaryRoutes = (
{
activeRuns,
lastRunStatuses,
resultReferenceResolver,
sessionBridge,
sessionMetadataStore,
sessionUiStateStore,
frontendActionCoordinator,
}: RegisterAuxiliaryRoutesOptions,
) => {
chatRouter.get("/result-ref/:result_ref", async (req, res) => {
const resultRef = req.params.result_ref?.trim();
const context = getLocalAgentContext(req);
const clientSessionId = typeof req.query.session_id === "string" ? req.query.session_id.trim() : undefined;
const offset = Math.max(0, Number.parseInt(String(req.query.offset ?? "0"), 10) || 0);
const limit = Math.min(500, Math.max(1, Number.parseInt(String(req.query.limit ?? "100"), 10) || 100));
if (!resultRef || !clientSessionId) {
res.status(400).json({ message: "result_ref and session_id are required" });
return;
}
const result = await resultReferenceResolver.getFullAuthorized(resultRef, {
actorKey: context.actorKey, clientSessionId, projectId: context.projectId,
}, { expectedKind: RESULT_REFERENCE_KIND.serverApiPayload });
if (!result) {
res.status(404).json({ message: "result_ref not found" });
return;
}
const rows = Array.isArray(result.data)
? result.data
: result.data && typeof result.data === "object" && Array.isArray((result.data as { data?: unknown }).data)
? (result.data as { data: unknown[] }).data
: [result.data];
res.json({ ...result, data: rows.slice(offset, offset + limit), page: { offset, limit, total: rows.length, has_more: offset + limit < rows.length } });
});
chatRouter.post("/abort", async (req, res) => {
const parsed = abortPayloadSchema.safeParse(req.body);
if (!parsed.success) {
@@ -98,6 +70,7 @@ export const registerChatAuxiliaryRoutes = (
return;
}
frontendActionCoordinator.cancelSession(sessionRecord.sessionId);
activeRuns.get(sessionRecord.sessionId)?.inputs?.close();
const binding = await sessionBridge.abort({
clientSessionId: sessionRecord.sessionId,
sessionId: sessionRecord.sessionId,
@@ -118,9 +91,9 @@ export const registerChatAuxiliaryRoutes = (
todos: cancelBackendTodos(message.todos),
}));
const currentState = await sessionUiStateStore.read(
toSessionUiStateContext(sessionRecord.sessionId),
toSessionUiStateContext(sessionRecord),
);
await sessionUiStateStore.write(toSessionUiStateContext(sessionRecord.sessionId), {
await sessionUiStateStore.write(toSessionUiStateContext(sessionRecord), {
sessionId: sessionRecord.sessionId,
isTitleManuallyEdited: currentState?.isTitleManuallyEdited ?? false,
messages: run.messages,
@@ -130,9 +103,9 @@ export const registerChatAuxiliaryRoutes = (
session_id: parsed.data.session_id,
message: "请求已中断",
});
subscriber.close();
}
run.subscribers.clear();
// The generation route emits its terminal Response after cleanup and
// closes subscribers. Closing here would truncate the Responses stream.
}
logger.info(