Files
openclaw/src/gateway/server-methods/talk-client.ts
2026-05-06 02:39:15 +01:00

258 lines
8.0 KiB
TypeScript

import { randomUUID } from "node:crypto";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import {
REALTIME_VOICE_AGENT_CONSULT_TOOL,
REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME,
buildRealtimeVoiceAgentConsultChatMessage,
} from "../../talk/agent-consult-tool.js";
import { resolveConfiguredRealtimeVoiceProvider } from "../../talk/provider-resolver.js";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
type ErrorShape,
validateTalkClientCreateParams,
validateTalkClientToolCallParams,
} from "../protocol/index.js";
import { registerTalkRealtimeRelayAgentRun } from "../talk-realtime-relay.js";
import { formatForLog } from "../ws-log.js";
import { chatHandlers } from "./chat.js";
import { asRecord } from "./record-shared.js";
import {
buildRealtimeInstructions,
buildTalkRealtimeConfig,
isUnsupportedBrowserWebRtcSession,
} from "./talk-shared.js";
import type { GatewayRequestHandlers } from "./types.js";
async function startRealtimeToolCallAgentConsult(params: {
sessionKey: string;
callId: string;
args: unknown;
relaySessionId?: string;
connId?: string;
request: Parameters<GatewayRequestHandlers[string]>[0];
}): Promise<
{ ok: true; runId: string; idempotencyKey: string } | { ok: false; error: ErrorShape }
> {
let message: string;
try {
message = buildRealtimeVoiceAgentConsultChatMessage(params.args);
} catch (err) {
return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, formatForLog(err)) };
}
const idempotencyKey = `talk-${params.callId}-${randomUUID()}`;
let chatResponse: { ok: true; result: unknown } | { ok: false; error: ErrorShape } | undefined;
await chatHandlers["chat.send"]({
...params.request,
req: {
type: "req",
id: `${params.request.req.id}:talk-tool-call`,
method: "chat.send",
},
params: {
sessionKey: params.sessionKey,
message,
idempotencyKey,
},
respond: (ok: boolean, result?: unknown, error?: ErrorShape) => {
chatResponse = ok
? { ok: true, result }
: {
ok: false,
error: error ?? errorShape(ErrorCodes.UNAVAILABLE, "chat.send failed without error"),
};
},
} as never);
if (!chatResponse) {
return {
ok: false,
error: errorShape(ErrorCodes.UNAVAILABLE, "chat.send did not return a realtime tool result"),
};
}
if (!chatResponse.ok) {
return { ok: false, error: chatResponse.error };
}
const runId = normalizeOptionalString(asRecord(chatResponse.result)?.runId) ?? idempotencyKey;
if (params.relaySessionId && params.connId) {
registerTalkRealtimeRelayAgentRun({
relaySessionId: params.relaySessionId,
connId: params.connId,
sessionKey: params.sessionKey,
runId,
});
}
return { ok: true, runId, idempotencyKey };
}
export const talkClientHandlers: GatewayRequestHandlers = {
"talk.client.create": async ({ params, respond, context }) => {
if (!validateTalkClientCreateParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.client.create params: ${formatValidationErrors(validateTalkClientCreateParams.errors)}`,
),
);
return;
}
const typedParams = params as {
provider?: string;
model?: string;
voice?: string;
mode?: string;
transport?: string;
brain?: string;
};
try {
const runtimeConfig = context.getRuntimeConfig();
const realtimeConfig = buildTalkRealtimeConfig(runtimeConfig, typedParams.provider);
const mode =
normalizeOptionalLowercaseString(typedParams.mode) ?? realtimeConfig.mode ?? "realtime";
if (mode !== "realtime") {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`talk.client.create only supports mode="realtime"; use talk.catalog for ${mode} provider discovery`,
),
);
return;
}
const brain =
normalizeOptionalLowercaseString(typedParams.brain) ??
realtimeConfig.brain ??
"agent-consult";
if (brain !== "agent-consult") {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`talk.client.create only supports brain="agent-consult"`,
),
);
return;
}
const transport =
normalizeOptionalLowercaseString(typedParams.transport) ?? realtimeConfig.transport;
if (transport === "managed-room") {
respond(
false,
undefined,
errorShape(
ErrorCodes.UNAVAILABLE,
"managed-room realtime Talk sessions are not available in the browser UI yet",
),
);
return;
}
if (transport === "gateway-relay") {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`talk.client.create is client-owned; use talk.session.create for gateway-relay`,
),
);
return;
}
const resolution = resolveConfiguredRealtimeVoiceProvider({
configuredProviderId: realtimeConfig.provider,
providerConfigs: realtimeConfig.providers,
cfg: runtimeConfig,
cfgForResolve: runtimeConfig,
noRegisteredProviderMessage: "No realtime voice provider registered",
});
if (resolution.provider.createBrowserSession && transport !== "gateway-relay") {
const session = await resolution.provider.createBrowserSession({
providerConfig: resolution.providerConfig,
instructions: buildRealtimeInstructions(),
tools: [REALTIME_VOICE_AGENT_CONSULT_TOOL],
model: normalizeOptionalString(typedParams.model) ?? realtimeConfig.model,
voice: normalizeOptionalString(typedParams.voice) ?? realtimeConfig.voice,
});
if (
!isUnsupportedBrowserWebRtcSession(session) &&
(!transport || session.transport === transport)
) {
respond(true, session, undefined);
return;
}
if (transport) {
respond(
false,
undefined,
errorShape(
ErrorCodes.UNAVAILABLE,
`Realtime provider "${resolution.provider.id}" does not support requested browser transport "${transport}"`,
),
);
return;
}
}
respond(
false,
undefined,
errorShape(
ErrorCodes.UNAVAILABLE,
`Realtime provider "${resolution.provider.id}" does not support client-owned realtime sessions`,
),
);
} catch (err) {
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));
}
},
"talk.client.toolCall": async (request) => {
const { params, respond } = request;
if (!validateTalkClientToolCallParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.client.toolCall params: ${formatValidationErrors(validateTalkClientToolCallParams.errors)}`,
),
);
return;
}
if (params.name !== REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, `unsupported realtime Talk tool: ${params.name}`),
);
return;
}
const result = await startRealtimeToolCallAgentConsult({
sessionKey: params.sessionKey,
callId: params.callId,
args: params.args ?? {},
relaySessionId: normalizeOptionalString(params.relaySessionId),
connId: normalizeOptionalString(request.client?.connId),
request,
});
if (!result.ok) {
respond(false, undefined, result.error);
return;
}
respond(
true,
{
runId: result.runId,
idempotencyKey: result.idempotencyKey,
},
undefined,
);
},
};