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
+76 -8
View File
@@ -19,6 +19,32 @@ const createEventStream = (events: unknown[]) => ({
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
describe("streamPromptResponse", () => {
it("flushes before prompting and resumes the same session when a message wins the completion race", async () => {
const order: string[] = [];
const emitted: string[] = [];
let idleChecks = 0;
const runtime = {
prompt: async (sessionId: string) => { order.push(`prompt:${sessionId}`); },
subscribeEvents: async () => createEventStream([1, 2].flatMap((round) => [
{ type: "message.part.updated", properties: { sessionID: "s", part: {
id: `text-${round}`, type: "text", text: `round ${round}`, sessionID: "s", messageID: `m-${round}`,
} } },
{ type: "session.idle", properties: { sessionID: "s" } },
])),
messages: async () => [],
} as unknown as OpencodeRuntimeAdapter;
const result = await streamPromptResponse({
runtime, sessionId: "s", clientSessionId: "client", message: "test",
beforePrompt: async () => { order.push("flush"); },
beforeIdle: async () => ++idleChecks === 2,
write: (event) => emitted.push(event),
});
expect(result.failed).toBeFalse();
expect(order).toEqual(["flush", "prompt:s", "prompt:s"]);
expect(idleChecks).toBe(2);
expect(emitted.filter((event) => event === "done")).toHaveLength(1);
});
it("keeps the first pending event when the V2 prompt acknowledgement arrives first", async () => {
const emitted: { event: string; data: Record<string, unknown> }[] = [];
const runtime = {
@@ -252,7 +278,7 @@ describe("streamPromptResponse", () => {
properties: {
id: "perm-1",
sessionID: "runtime-session-1",
permission: "web_search",
permission: "websearch",
patterns: ["npm test"],
metadata: { command: "npm test" },
always: ["npm test"],
@@ -286,18 +312,60 @@ describe("streamPromptResponse", () => {
{
requestId: "perm-1",
sessionId: "runtime-session-1",
reply: "always",
reply: "once",
},
]);
expect(events.some((item) => item.event === "permission_request")).toBe(false);
expect(events.find((item) => item.event === "permission_response")?.data).toEqual({
session_id: "client-session-1",
request_id: "perm-1",
reply: "always",
reply: "once",
});
});
it("requires manual Shell approval even in always mode", async () => {
it.each(["permission.asked", "permission.v2.asked"])("returns to manual approval after an automatic round for %s", async (eventType) => {
let remembered = false;
let round = 0;
const replies: string[] = [];
const runtime = {
subscribeEvents: async () => {
round += 1;
return createEventStream([
...(!remembered ? [{
type: eventType,
properties: {
id: `permission-${round}`,
sessionID: "runtime-session-1",
...(eventType === "permission.asked"
? { permission: "webfetch", patterns: ["https://example.test/*"], always: ["https://example.test/*"] }
: { action: "webfetch", resources: ["https://example.test/"], save: ["https://example.test/*"] }),
},
}] : []),
{ type: "session.idle", properties: { sessionID: "runtime-session-1" } },
]);
},
prompt: async () => undefined,
messages: async () => [],
replyPermission: async ({ reply }: { reply: string }) => {
replies.push(reply);
remembered = reply === "always";
},
} as unknown as OpencodeRuntimeAdapter;
const automatic: Array<{ event: string; data: Record<string, unknown> }> = [];
const manual: Array<{ event: string; data: Record<string, unknown> }> = [];
const options = { runtime, sessionId: "runtime-session-1", clientSessionId: "client-session-1", message: "fetch a public reference" };
await streamPromptResponse({ ...options, approvalMode: "always", write: (event, data) => automatic.push({ event, data }) });
await streamPromptResponse({ ...options, approvalMode: "request", write: (event, data) => manual.push({ event, data }) });
expect(replies).toEqual(["once"]);
expect(automatic.find(({ event }) => event === "permission_response")?.data.reply).toBe("once");
expect(manual.find(({ event }) => event === "permission_request")?.data).toMatchObject({
request_id: "permission-2", permission: "webfetch",
});
});
it.each(["shell", "bash", "edit", "mcp_fixture_write"])("automatically approves %s in always mode", async (action) => {
const replies: Array<Record<string, unknown>> = [];
const runtime = {
subscribeEvents: async () =>
@@ -307,7 +375,7 @@ describe("streamPromptResponse", () => {
properties: {
id: "perm-1",
sessionID: "runtime-session-1",
permission: "bash",
permission: action,
patterns: ["npm test"],
metadata: { command: "npm test" },
always: ["npm test"],
@@ -337,9 +405,9 @@ describe("streamPromptResponse", () => {
write: (event, data) => events.push({ event, data }),
});
expect(replies).toEqual([]);
expect(events.some((item) => item.event === "permission_request")).toBe(true);
expect(events.some((item) => item.event === "permission_response")).toBe(false);
expect(replies).toEqual([{ requestId: "perm-1", sessionId: "runtime-session-1", reply: "once" }]);
expect(events.some((item) => item.event === "permission_request")).toBe(false);
expect(events.find((item) => item.event === "permission_response")?.data.reply).toBe("once");
});
it("forwards opencode v2 permission requests as SSE payloads", async () => {