Files
next-tjwater-agent/src/product/agent-messages.ts
T
jiang 6a49d2209e 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.
2026-10-19 18:46:58 +08:00

77 lines
4.4 KiB
TypeScript

import { z } from "zod";
import { config } from "../config.js";
import { requestWithSessionCredential } from "./credential-refresh.js";
import type { ProductCredential } from "./server-auth-client.js";
import { OperationalTaskClientError } from "./operational-task-client.js";
export const agentMessageSchema = z.object({
message_id: z.string().uuid(), project_id: z.string().uuid(),
task_id: z.string().uuid(), run_id: z.string().uuid(), request_id: z.string().uuid(),
source_session_id: z.string(), source_owner_id: z.string(), conversation_owner_id: z.string(),
target_session_id: z.string().nullable(), direction: z.enum(["to_task", "to_chat"]),
in_reply_to: z.string().uuid().nullable(), text: z.string(),
status: z.enum(["pending", "enqueued", "delivered", "failed"]),
native_message_id: z.string(), failure_detail: z.string().nullable(),
created_at: z.string(), updated_at: z.string(), delivered_at: z.string().nullable(),
});
export type AgentMessage = z.infer<typeof agentMessageSchema>;
export const agentMessagePageSchema = z.object({ items: z.array(agentMessageSchema), next_cursor: z.string().nullable() });
export type MessageCredential = string | ProductCredential;
/** Business records are Server-owned; credentials never become model arguments. */
export class AgentMessageClient {
constructor(private readonly baseUrl = config.TJWATER_SERVER_BASE_URL) {}
async request(credential: MessageCredential, path: string, method = "GET", body?: unknown): Promise<unknown> {
const execute = (value: ProductCredential) => fetch(new URL(path, this.baseUrl), {
method, signal: AbortSignal.timeout(config.TJWATER_SERVER_TIMEOUT_MS),
headers: { Authorization: value.authorization, "X-Project-Id": value.projectId, "Content-Type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const response = typeof credential === "string"
? await requestWithSessionCredential(credential, execute) : await execute(credential);
const value: unknown = await response.json();
if (!response.ok) {
const problem = z.object({ code: z.string().optional(), message: z.string().optional(), detail: z.unknown().optional() }).passthrough().safeParse(value);
const nested = problem.success ? z.object({ code: z.string().optional(), message: z.string().optional() }).safeParse(problem.data.detail) : null;
throw new OperationalTaskClientError(response.status,
(nested?.success ? nested.data.message : undefined) ?? (problem.success ? problem.data.message : undefined) ?? "Agent message request failed",
(nested?.success ? nested.data.code : undefined) ?? (problem.success ? problem.data.code : undefined));
}
return value;
}
async list(credential: MessageCredential, sessionId: string, cursor?: string, limit = 50, pendingOnly = false) {
const query = new URLSearchParams({ session_id: sessionId, limit: String(limit) });
if (cursor) query.set("cursor", cursor);
if (pendingOnly) query.set("pending_only", "true");
return agentMessagePageSchema.parse(await this.request(credential, `/api/v1/agent-messages?${query}`));
}
async get(credential: MessageCredential, id: string) {
return agentMessageSchema.parse(await this.request(credential, `/api/v1/agent-messages/${encodeURIComponent(id)}`));
}
async send(sessionId: string, input: { request_id: string; task_id: string; run_id: string; message: string }) {
return agentMessageSchema.parse(await this.request(sessionId, "/api/v1/agent-messages", "POST", {
request_id: input.request_id, task_id: input.task_id, run_id: input.run_id,
source_session_id: sessionId, text: input.message,
}));
}
async reply(sessionId: string, input: { request_id: string; message_id: string; message: string }) {
return agentMessageSchema.parse(await this.request(sessionId, `/api/v1/agent-messages/${input.message_id}/reply`, "POST", {
request_id: input.request_id, source_session_id: sessionId, text: input.message,
}));
}
async receipt(credential: MessageCredential, item: AgentMessage, status: AgentMessage["status"], target: string | null, failure?: string) {
return agentMessageSchema.parse(await this.request(credential, `/api/v1/agent-messages/${item.message_id}/delivery`, "POST", {
status, target_session_id: target, native_message_id: item.native_message_id,
failure_detail: failure ?? null,
}));
}
}
export const agentMessageClient = new AgentMessageClient();