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
+126
View File
@@ -0,0 +1,126 @@
import { randomUUID } from "node:crypto";
import type { UIMessageChunk } from "ai";
import { z } from "zod";
import { agentTriggerContextSchema } from "../routes/agentTrigger.js";
const text = z.string().min(1).max(10000);
const inputMessage = z.object({
type: z.literal("message").optional(), role: z.literal("user"),
content: z.union([text, z.array(z.object({ type: z.literal("input_text"), text }).strict()).min(1).max(20)]),
}).strict();
// A Responses text-generation surface backed by the owned OpenCode session.
// Unsupported OpenAI options fail validation instead of being silently ignored.
export const responseRequestSchema = z.object({
input: z.union([text, z.array(inputMessage).min(1).max(20)]),
model: z.string().min(1).optional(),
stream: z.boolean().default(false),
conversation: z.union([z.string().min(1).max(128), z.object({ id: z.string().min(1).max(128) }).strict()]).optional(),
metadata: z.record(z.string().max(64), z.string().max(512)).refine((value) => Object.keys(value).length <= 16).optional(),
tjwater: z.object({
delivery: z.enum(["steer", "queue"]).optional(),
input_id: z.string().regex(/^msg_[a-f0-9]{32}$/).optional(),
approval_mode: z.enum(["request", "always"]).default("request"),
capabilities: z.array(z.string().max(64)).max(20).default([]),
trigger_context: agentTriggerContextSchema.optional(),
task_context: z.object({
report_revision: z.number().int().positive().optional(),
evidence_id: z.string().min(1).max(128).optional(), gap_id: z.string().min(1).max(128).optional(),
}).strict().optional(),
}).strict().default({}),
}).strict();
export function responsePrompt(input: z.infer<typeof responseRequestSchema>["input"]) {
return typeof input === "string" ? input : input.map((message) => typeof message.content === "string"
? message.content : message.content.map((part) => part.text).join("\n")).join("\n\n");
}
type OutputText = { type: "output_text"; text: string; annotations: unknown[]; logprobs: unknown[] };
type OutputMessage = { id: string; type: "message"; status: "in_progress" | "completed" | "incomplete"; role: "assistant"; content: OutputText[] };
export type ResponseObject = {
id: string; object: "response"; created_at: number; completed_at: number | null;
status: "in_progress" | "completed" | "failed" | "incomplete"; model: string;
conversation: { id: string }; output: OutputMessage[]; usage: null;
error: { code: string; message: string } | null; incomplete_details: { reason: string } | null;
metadata: Record<string, string>;
};
export type ResponseEvent = Record<string, unknown> & { type: string; sequence_number: number };
export function createResponsesProjection(options: {
model: string; conversationId: string; metadata?: Record<string, string>;
emit: (event: ResponseEvent) => void;
}) {
const response: ResponseObject = {
id: `resp_${randomUUID().replaceAll("-", "")}`, object: "response", created_at: Math.floor(Date.now() / 1000),
completed_at: null, status: "in_progress", model: options.model, conversation: { id: options.conversationId },
output: [], usage: null, error: null, incomplete_details: null, metadata: options.metadata ?? {},
};
let sequence = 0;
let started = false;
const textIndices = new Map<string, number>();
const closedText = new Set<string>();
const emit = (type: string, value: Record<string, unknown>) => options.emit({ type, sequence_number: sequence++, ...structuredClone(value) });
const begin = () => {
if (started) return;
started = true;
emit("response.created", { response });
emit("response.in_progress", { response });
};
const message = (id?: string) => {
if (!response.output.length) {
response.output.push({ id: id ?? `msg_${randomUUID().replaceAll("-", "")}`, type: "message", status: "in_progress", role: "assistant", content: [] });
emit("response.output_item.added", { output_index: 0, item: response.output[0] });
}
return response.output[0]!;
};
const openText = (id: string) => {
const item = message();
if (!textIndices.has(id)) {
const index = item.content.length;
textIndices.set(id, index);
item.content.push({ type: "output_text", text: "", annotations: [], logprobs: [] });
emit("response.content_part.added", { item_id: item.id, output_index: 0, content_index: index, part: item.content[index] });
}
return { item, index: textIndices.get(id)! };
};
const endText = (id: string) => {
if (!textIndices.has(id) || closedText.has(id)) return;
const { item, index } = openText(id);
closedText.add(id);
emit("response.output_text.done", { item_id: item.id, output_index: 0, content_index: index, text: item.content[index]!.text, logprobs: [] });
emit("response.content_part.done", { item_id: item.id, output_index: 0, content_index: index, part: item.content[index] });
};
const finish = (reason: "completed" | "failed" | "cancelled" = "completed") => {
if (response.status !== "in_progress") return;
begin();
for (const id of textIndices.keys()) endText(id);
for (const item of response.output) {
item.status = reason === "completed" ? "completed" : "incomplete";
emit("response.output_item.done", { output_index: 0, item });
}
response.status = reason === "cancelled" ? "incomplete" : reason;
if (reason === "completed") response.completed_at = Math.floor(Date.now() / 1000);
emit(`response.${response.status}`, { response });
};
return {
response, begin, finish,
write(chunk: UIMessageChunk) {
if (response.status !== "in_progress") return;
begin();
if (chunk.type === "start") message(chunk.messageId);
else if (chunk.type === "text-start") openText(chunk.id);
else if (chunk.type === "text-delta") {
const { item, index } = openText(chunk.id);
item.content[index]!.text += chunk.delta;
emit("response.output_text.delta", { item_id: item.id, output_index: 0, content_index: index, delta: chunk.delta, logprobs: [] });
} else if (chunk.type === "text-end") endText(chunk.id);
else if (chunk.type === "error") response.error = { code: "agent_execution_error", message: chunk.errorText };
else if (chunk.type.startsWith("data-")) {
const data = chunk as { type: string; data: unknown; id?: string; transient?: boolean };
emit(`tjwater.${chunk.type.slice(5)}`, { response_id: response.id, data: data.data,
...(data.id ? { id: data.id } : {}), ...(data.transient ? { transient: true } : {}) });
}
// Finalization belongs to the route, after task completion and persistence.
},
};
}