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.
77 lines
4.4 KiB
TypeScript
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();
|