blob: 1ce9d7a3ec930c9f94526cea64d34b2d0c65a907 [file]
import { Buffer } from "node:buffer";
import type { ComputerUseToolSet, MakaTool } from "@maka/runtime";
import {
createOAuthPresentationClientProvider,
type ClientCapabilityProvider,
type OAuthPresentationBackend,
} from "@maka/runtime-host/client";
import type {
ClientCapabilityCallFrame,
ClientCapabilityCallResult,
ClientCapabilityContentBlock,
ClientCapabilityOffer,
} from "@maka/runtime-host/protocol";
import { toJSONSchema, z } from "zod";
const CAPABILITY_VERSION = "0";
const BROWSER_OFFER_ID = "desktop_browser";
const COMPUTER_USE_OFFER_ID = "desktop_computer_use";
export interface DesktopCapabilityGroup {
readonly offerId: string;
readonly label: string;
readonly description: string;
readonly tools: readonly MakaTool[];
}
interface NativeToolBinding {
readonly tool: MakaTool;
}
type DesktopToolModelOutput = Awaited<
ReturnType<NonNullable<MakaTool["toModelOutput"]>>
>;
type DesktopToolContentPart = Extract<
DesktopToolModelOutput,
{ type: "content" }
>["value"][number];
export interface DesktopNativeCapabilityProviderInput {
readonly browserTools: readonly MakaTool[];
readonly releaseBrowserSession: (sessionId: string) => void | Promise<void>;
readonly computerUseTools: ComputerUseToolSet;
readonly releaseComputerUseSession: (
sessionId: string,
) => void | Promise<void>;
readonly oauthPresentation?: OAuthPresentationBackend;
readonly additionalGroups?: () => readonly DesktopCapabilityGroup[];
}
export interface DesktopNativeCapabilityProvider extends ClientCapabilityProvider {
abortSession(sessionId: string): Promise<void>;
releaseSession(sessionId: string): Promise<void>;
close(): Promise<void>;
}
/** Adapt Desktop-owned Maka tools to the open Client Capability protocol. */
export function createDesktopNativeCapabilityProvider(
input: DesktopNativeCapabilityProviderInput,
providerOptions: {
readonly releaseResourcesOnClose?: boolean;
readonly onSessionUsed?: (sessionId: string) => void;
readonly onComputerUseTurnUsed?: (
sessionId: string,
turnId: string,
) => void;
readonly onClosed?: () => void;
} = {},
): DesktopNativeCapabilityProvider {
const groups = capabilityGroups(input);
const offers = Object.freeze(groups.map(capabilityOffer));
const bindings = indexBindings(groups);
const oauthPresentation = input.oauthPresentation
? createOAuthPresentationClientProvider(input.oauthPresentation)
: undefined;
const releaseSessionResources = [
input.releaseBrowserSession,
input.releaseComputerUseSession,
] as const;
const activeInvocations = new Map<
AbortController,
{ readonly sessionId: string; readonly settled: Promise<void> }
>();
const usedSessionIds = new Set<string>();
let closed = false;
let closeTask: Promise<void> | undefined;
function close(): Promise<void> {
if (closeTask) return closeTask;
closed = true;
closeTask = closeProvider(
activeInvocations,
usedSessionIds,
releaseSessionResources,
providerOptions.releaseResourcesOnClose !== false,
).finally(() => providerOptions.onClosed?.());
return closeTask;
}
return {
offers: () => offers,
services: () => oauthPresentation?.services?.() ?? [],
call: (frame, options) => {
if (closed)
throw new Error("Desktop native capability provider is closed");
const binding = bindings.get(bindingKey(frame));
if (!binding) throw new Error("Desktop native capability is not offered");
const invocation = new AbortController();
const task = invokeNativeTool(
binding,
frame,
options,
providerOptions,
invocation,
usedSessionIds,
);
const settled = task.then(
() => undefined,
() => undefined,
);
activeInvocations.set(invocation, {
sessionId: frame.sessionId,
settled,
});
void settled.finally(() => activeInvocations.delete(invocation));
return task;
},
callService: (frame, options) => {
if (closed)
throw new Error("Desktop native capability provider is closed");
if (!oauthPresentation?.callService) {
throw new Error("Desktop native capability service is not offered");
}
return oauthPresentation.callService(frame, options);
},
abortSession: async (sessionId) => {
const settling = abortInvocations(activeInvocations, sessionId);
await Promise.all(settling);
},
releaseSession: async (sessionId) => {
const settling = abortInvocations(activeInvocations, sessionId);
await Promise.all(settling);
usedSessionIds.delete(sessionId);
await settleSessionReleases(releaseSessionResources, [sessionId]);
},
close,
};
}
async function closeProvider(
activeInvocations: ReadonlyMap<
AbortController,
{ readonly sessionId: string; readonly settled: Promise<void> }
>,
usedSessionIds: Set<string>,
releases: readonly ((sessionId: string) => void | Promise<void>)[],
releaseResources: boolean,
): Promise<void> {
const settling: Promise<void>[] = [];
for (const [invocation, active] of activeInvocations) {
invocation.abort(new Error("Desktop native capability provider closed"));
settling.push(active.settled);
}
await Promise.all(settling);
const sessionIds = [...usedSessionIds];
usedSessionIds.clear();
if (releaseResources) await settleSessionReleases(releases, sessionIds);
}
async function settleSessionReleases(
releases: readonly ((sessionId: string) => void | Promise<void>)[],
sessionIds: readonly string[],
): Promise<void> {
const results = await Promise.allSettled(
sessionIds.flatMap((sessionId) =>
releases.map(async (release) => release(sessionId)),
),
);
const failed = results.find(
(result): result is PromiseRejectedResult => result.status === "rejected",
);
if (failed) throw failed.reason;
}
function capabilityGroups(
input: DesktopNativeCapabilityProviderInput,
): DesktopCapabilityGroup[] {
return [
...(input.browserTools.length > 0
? [
{
offerId: BROWSER_OFFER_ID,
label: "Browser",
description:
"Operate the embedded browser owned by this Desktop client.",
tools: input.browserTools,
},
]
: []),
...(input.computerUseTools.length > 0
? [
{
offerId: COMPUTER_USE_OFFER_ID,
label: "Computer Use",
description:
"Observe and operate the desktop through this Desktop client.",
tools: input.computerUseTools,
},
]
: []),
...(input.additionalGroups?.() ?? []),
];
}
async function invokeNativeTool(
binding: NativeToolBinding,
frame: ClientCapabilityCallFrame,
options: Parameters<NonNullable<ClientCapabilityProvider["call"]>>[1],
providerOptions: {
readonly onSessionUsed?: (sessionId: string) => void;
readonly onComputerUseTurnUsed?: (
sessionId: string,
turnId: string,
) => void;
},
invocation: AbortController,
usedSessionIds: Set<string>,
): Promise<ClientCapabilityCallResult> {
const signal = AbortSignal.any([options.signal, invocation.signal]);
signal.throwIfAborted();
const parameters = requireZodSchema(binding.tool);
const args = await parameters.parseAsync(frame.arguments);
signal.throwIfAborted();
await options.accept();
signal.throwIfAborted();
usedSessionIds.add(frame.sessionId);
providerOptions.onSessionUsed?.(frame.sessionId);
if (frame.offerId === COMPUTER_USE_OFFER_ID) {
providerOptions.onComputerUseTurnUsed?.(frame.sessionId, frame.turnId);
}
const output = await binding.tool.impl(args, {
sessionId: frame.sessionId,
turnId: frame.turnId,
cwd: frame.cwd,
toolCallId: frame.toolCallId,
abortSignal: signal,
emitOutput() {},
});
return projectToolResult(binding.tool, frame.toolCallId, args, output);
}
function abortInvocations(
activeInvocations: ReadonlyMap<
AbortController,
{ readonly sessionId: string; readonly settled: Promise<void> }
>,
sessionId: string,
): Promise<void>[] {
const settling: Promise<void>[] = [];
for (const [invocation, active] of activeInvocations) {
if (active.sessionId !== sessionId) continue;
invocation.abort(new Error("Desktop native capability Session released"));
settling.push(active.settled);
}
return settling;
}
function capabilityOffer(group: DesktopCapabilityGroup): ClientCapabilityOffer {
return Object.freeze({
offerId: group.offerId,
version: CAPABILITY_VERSION,
affinity: "session",
label: group.label,
description: group.description,
tools: Object.freeze(
group.tools.map((tool) =>
Object.freeze({
serverId: group.offerId,
name: tool.name,
description: tool.description,
inputSchema: toolInputSchema(tool),
...(tool.displayName
? { annotations: Object.freeze({ title: tool.displayName }) }
: {}),
}),
),
),
});
}
function toolInputSchema(tool: MakaTool): Record<string, unknown> {
const schema = toJSONSchema(requireZodSchema(tool), {
io: "input",
target: "draft-07",
unrepresentable: "any",
cycles: "ref",
reused: "inline",
});
delete schema.$schema;
if (schema.type !== "object") {
throw new Error(
`Desktop native capability tool schema must be an object: ${tool.name}`,
);
}
return Object.freeze(schema);
}
function requireZodSchema(tool: MakaTool): z.ZodType {
if (!(tool.parameters instanceof z.ZodType)) {
throw new Error(
`Desktop native capability tool has an invalid schema: ${tool.name}`,
);
}
return tool.parameters;
}
function indexBindings(
groups: readonly DesktopCapabilityGroup[],
): Map<string, NativeToolBinding> {
const bindings = new Map<string, NativeToolBinding>();
for (const group of groups) {
for (const tool of group.tools) {
const key = bindingKey({
offerId: group.offerId,
serverId: group.offerId,
toolName: tool.name,
});
if (bindings.has(key)) {
throw new Error(
`Duplicate Desktop native capability tool: ${group.offerId}/${tool.name}`,
);
}
bindings.set(key, { tool });
}
}
return bindings;
}
function bindingKey(
frame: Pick<ClientCapabilityCallFrame, "offerId" | "serverId" | "toolName">,
): string {
return `${frame.offerId}\0${frame.serverId}\0${frame.toolName}`;
}
async function projectToolResult(
tool: MakaTool,
toolCallId: string,
input: unknown,
output: unknown,
): Promise<ClientCapabilityCallResult> {
const modelOutput = tool.toModelOutput
? await tool.toModelOutput({
toolCallId,
input,
output,
})
: undefined;
if (!modelOutput) {
return typeof output === "string"
? { content: [{ type: "text", text: output }] }
: { content: [], structuredContent: output };
}
switch (modelOutput.type) {
case "text":
case "error-text":
return { content: [{ type: "text", text: modelOutput.value }] };
case "json":
case "error-json":
return { content: [], structuredContent: modelOutput.value };
case "execution-denied":
return {
content: [
{ type: "text", text: modelOutput.reason ?? "Execution denied" },
],
};
case "content":
return { content: modelOutput.value.map(projectContentPart) };
}
}
function projectContentPart(
part: DesktopToolContentPart,
): ClientCapabilityContentBlock {
switch (part.type) {
case "text":
return { type: "text", text: part.text };
case "file":
if (part.data.type !== "data") {
throw new Error(
"Desktop native capability cannot return referenced or URL files",
);
}
return projectBinaryContent(part.data.data, part.mediaType);
case "file-data":
case "image-data":
return projectBinaryContent(part.data, part.mediaType);
default:
throw new Error(
`Desktop native capability cannot return ${part.type} content`,
);
}
}
function projectBinaryContent(
data: string | Uint8Array | ArrayBuffer | Buffer,
mimeType: string,
): ClientCapabilityContentBlock {
const encoded =
typeof data === "string"
? data
: Buffer.from(
data instanceof ArrayBuffer ? new Uint8Array(data) : data,
).toString("base64");
if (mimeType.startsWith("image/"))
return { type: "image", data: encoded, mimeType };
if (mimeType.startsWith("audio/"))
return { type: "audio", data: encoded, mimeType };
throw new Error(
`Desktop native capability cannot return file type ${mimeType}`,
);
}