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
+65 -17
View File
@@ -6,7 +6,10 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildChatRouter } from "../../src/routes/chat.js";
import { ChatSessionBridge } from "../../src/chat/sessionBridge.js";
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
import { SessionUiStateStore } from "../../src/sessions/uiStateStore.js";
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
import { toActorKey, toProjectKey } from "../../src/utils/fileStore.js";
const originalFetch = globalThis.fetch;
@@ -42,7 +45,9 @@ describe("chat session HTTP isolation", () => {
});
}) as typeof fetch;
const sessionMetadataStore = new SessionMetadataStore(metadataDir);
const workspaces = new SessionWorkspaceStore(join(metadataDir, "workspaces"));
const sessionMetadataStore = new SessionMetadataStore(metadataDir, workspaces);
const sessionUiStateStore = new SessionUiStateStore(join(metadataDir, "old-ui"), workspaces);
await sessionMetadataStore.initialize();
const ownerContext = {
actorKey: toActorKey("product:user-a"),
@@ -54,6 +59,14 @@ describe("chat session HTTP isolation", () => {
...ownerContext,
sessionId: "ordinary-session",
});
await sessionUiStateStore.write({ ...ownerContext, sessionId: "ordinary-session" }, {
sessionId: "ordinary-session", messages: [{ type: "text", text: "owner-only answer" }],
});
const personalContext = { actorKey: ownerContext.actorKey, projectKey: `personal-${ownerContext.actorKey}`, userId: "user-a" };
await sessionMetadataStore.ensure({ ...personalContext, sessionId: "personal-session" });
await sessionUiStateStore.write({ ...personalContext, sessionId: "personal-session" }, {
sessionId: "personal-session", messages: [{ type: "text", text: "personal answer" }],
});
await sessionMetadataStore.ensure({
...ownerContext,
sessionId: "execution-session",
@@ -63,32 +76,64 @@ describe("chat session HTTP isolation", () => {
});
const app = express();
const removed: string[] = [];
let failCleanup = true;
const runtime = {
removeSession: async (sessionId: string) => {
if (failCleanup) throw new Error("native service unavailable");
removed.push(sessionId);
},
} as never;
app.use(express.json());
app.use("/api/v1/agent/chat", buildChatRouter({
frontendActionCoordinator: {} as never,
learningOrchestrator: {} as never,
app.use("/api/v1", buildChatRouter({
frontendActionCoordinator: { cancelSession: () => undefined } as never,
learningOrchestrator: { cancelSession: async () => undefined } as never,
memoryStore: {} as never,
resultReferenceResolver: {} as never,
runtime: {} as never,
sessionBridge: {} as never,
runtime,
sessionBridge: new ChatSessionBridge(runtime),
sessionMetadataStore,
sessionTranscriptStore: {} as never,
sessionUiStateStore: {} as never,
sessionUiStateStore,
}));
server = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server.once("listening", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("expected HTTP server address");
const ownedOrdinary = await getJson(address.port, "/api/v1/agent/chat/sessions", "token-a", ownerContext.projectId);
const ownedExecution = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-a", ownerContext.projectId);
const otherUser = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-b", ownerContext.projectId);
const otherProject = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-a", "10000000-0000-4000-8000-000000000002");
const ownedOrdinary = await getJson(address.port, "/api/v1/sessions", "token-a", ownerContext.projectId);
const ownedExecution = await getJson(address.port, "/api/v1/sessions?purpose=task_execution", "token-a", ownerContext.projectId);
const otherUser = await getJson(address.port, "/api/v1/sessions?purpose=task_execution", "token-b", ownerContext.projectId);
const otherProject = await getJson(address.port, "/api/v1/sessions?purpose=task_execution", "token-a", "10000000-0000-4000-8000-000000000002");
expect(ownedOrdinary.body.sessions.map((item: { id: string }) => item.id)).toEqual(["ordinary-session"]);
expect(ownedExecution.body.sessions.map((item: { id: string }) => item.id)).toEqual(["execution-session"]);
expect(otherUser.body.sessions).toEqual([]);
expect(otherProject.body.sessions).toEqual([]);
expect((await getJson(address.port, "/api/v1/session/ordinary-session", "token-a", ownerContext.projectId)).body.messages).toEqual([{ type: "text", text: "owner-only answer" }]);
expect((await getJson(address.port, "/api/v1/session/ordinary-session", "token-b", ownerContext.projectId)).status).toBe(404);
expect((await getJson(address.port, "/api/v1/session/ordinary-session", "token-a", "10000000-0000-4000-8000-000000000002")).status).toBe(404);
expect((await getJson(address.port, "/api/v1/session/ordinary-session", "token-a")).status).toBe(404);
const personalHistory = await getJson(address.port, "/api/v1/sessions", "token-a");
expect(personalHistory.body.sessions.map((session: { id: string }) => session.id)).toEqual(["personal-session"]);
expect((await getJson(address.port, "/api/v1/session/personal-session", "token-a")).body.messages).toEqual([{ type: "text", text: "personal answer" }]);
expect((await getJson(address.port, "/api/v1/session/personal-session", "token-b")).status).toBe(404);
expect((await getJson(address.port, "/api/v1/session/personal-session", "token-a", ownerContext.projectId)).status).toBe(404);
expect((await getJson(address.port, "/api/v1/session", "token-a", undefined, { parent_session_id: "ordinary-session" })).status).toBe(404);
expect((await getJson(address.port, "/api/v1/fork", "token-b", ownerContext.projectId, { session_id: "ordinary-session", keep_message_count: 2 })).status).toBe(404);
expect((await getJson(address.port, "/api/v1/session", "token-b", ownerContext.projectId, { session_id: "ordinary-session" })).status).toBe(404);
expect((await getJson(address.port, "/api/v1/session", "token-b", ownerContext.projectId, { session_id: "unknown-session" })).status).toBe(404);
const path = "/api/v1/session/ordinary-session";
expect((await getJson(address.port, path, "token-b", ownerContext.projectId, undefined, "DELETE")).status).toBe(204);
expect((await getJson(address.port, path, "token-a", undefined, undefined, "DELETE")).status).toBe(204);
expect((await sessionUiStateStore.read({ ...ownerContext, sessionId: "ordinary-session" }))?.messages).toHaveLength(1);
expect((await getJson(address.port, path, "token-a", ownerContext.projectId, undefined, "DELETE")).status).toBe(503);
expect((await getJson(address.port, path, "token-a", ownerContext.projectId)).status).toBe(404);
expect((await getJson(address.port, "/api/v1/sessions", "token-a", ownerContext.projectId)).body.sessions).toEqual([]);
failCleanup = false;
expect((await getJson(address.port, path, "token-a", ownerContext.projectId, undefined, "DELETE")).status).toBe(204);
expect(removed).toEqual(["ordinary-session"]);
expect(await sessionUiStateStore.read({ ...ownerContext, sessionId: "ordinary-session" })).toBeNull();
expect((await getJson(address.port, "/api/v1/session/personal-session", "token-a")).status).toBe(200);
});
});
@@ -96,16 +141,19 @@ const getJson = (
port: number,
path: string,
token: string,
projectId: string,
projectId?: string,
body?: Record<string, unknown>,
method?: string,
) => new Promise<{ status: number; body: Record<string, any> }>((resolve, reject) => {
const request = httpRequest({
hostname: "127.0.0.1",
port,
path,
method: "GET",
method: method ?? (body ? "POST" : "GET"),
headers: {
Authorization: `Bearer ${token}`,
"X-Project-Id": projectId,
...(projectId ? { "X-Project-Id": projectId } : {}),
...(body ? { "Content-Type": "application/json" } : {}),
},
}, (response) => {
const chunks: Buffer[] = [];
@@ -113,10 +161,10 @@ const getJson = (
response.on("end", () => {
resolve({
status: response.statusCode ?? 0,
body: JSON.parse(Buffer.concat(chunks).toString("utf8")),
body: JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"),
});
});
});
request.on("error", reject);
request.end();
request.end(body ? JSON.stringify(body) : undefined);
});