import type { ConnectorTool, MediaCapabilities, MediaRuntime } from "@rakazo/adapter-kit"; import type { MediaOperation } from "@rakazo/contracts"; import { MediaOperationSchema } from "@rakazo/contracts"; import { MediaJobRejectedError } from "./media-runtime.js"; /** Media tools are named exactly like their operations and dispatched by one module. */ const MEDIA_TOOL_NAMES: ReadonlySet = new Set(MediaOperationSchema.options); export function isMediaToolName(name: string): name is MediaOperation { return MEDIA_TOOL_NAMES.has(name); } /** The shared `continue_after` property every media tool definition accepts. */ export const MEDIA_CONTINUE_AFTER_PROPERTY = { continue_after: { type: "boolean", description: "Wake me with the result when the job ends so I can continue a multi-step task. Default false: the media card shows the result to the user.", }, } as const; /** A media tool's JSON schema: its own properties plus `continue_after`. */ export function mediaToolInputSchema(schema: { properties: Record; required?: string[]; }): ConnectorTool["inputSchema"] { return { type: "object", properties: { ...schema.properties, ...MEDIA_CONTINUE_AFTER_PROPERTY }, ...(schema.required?.length ? { required: schema.required } : {}), }; } /** Drops media tools whose operation the space cannot run right now. */ export function selectMediaTools( tools: T[], capabilities: MediaCapabilities | undefined, ): T[] { return tools.filter((tool) => !isMediaToolName(tool.name) || capabilities?.[tool.name] === true); } /** Starts the job behind a media tool call; the turn never waits for the job itself. */ export async function executeMediaTool( media: MediaRuntime | undefined, scope: { spaceId: string; userId: string; botId: string; groupId: string | null; threadId: string; runId: string; /** The tool call's effect key, so a replayed call returns the job it started. */ operationKey: string; }, operation: MediaOperation, args: Record, ) { if (!media) return { error: `${operation} is not available.` }; const { continue_after: continueAfter, ...input } = args; try { const started = await media.start({ operation, input, ...scope, continueAfter: continueAfter === true, }); return { jobId: started.jobId, status: "queued", note: continueAfter === true ? "The job runs in the background. You are woken with its result when it ends; do not poll or wait." : "The job runs in the background and its card shows progress and the result to the user. Do not poll or wait.", }; } catch (error) { if (error instanceof MediaJobRejectedError) return { error: error.message }; throw error; } }