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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user