import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import type { Sql } from '../db.js'; import type { Broker } from '@boocode/server/broker'; import type { WsFrame } from '@boocode/server/ws-frames'; import { getSkillBody } from '@boocode/server/skills'; import { buildSkillInvokeSyntheticFrames, buildSkillInvokeUserFrames, DEFAULT_SKILL_USER_MESSAGE, runSkillInvokeTransaction, } from '@boocode/server/skill-invoke'; import { resolveChatId } from './chat-resolve.js'; const SkillInvokeBody = z.object({ pane_id: z.string().min(1).max(200), skill_name: z.string().min(1), user_message: z.string().max(64_000).nullable().optional(), }); interface InferenceApi { enqueue: (sessionId: string, chatId: string, assistantId: string, user: string) => void; hasActive: (chatId: string) => boolean; } export function registerSkillRoutes( app: FastifyInstance, sql: Sql, broker: Broker, inference: InferenceApi, ): void { app.post<{ Params: { sessionId: string } }>( '/api/sessions/:sessionId/skill_invoke', async (req, reply) => { const parsed = SkillInvokeBody.safeParse(req.body); if (!parsed.success) { reply.code(400); return { error: 'invalid body', details: parsed.error.flatten() }; } const sessionId = req.params.sessionId; const { pane_id, skill_name } = parsed.data; const sessionRows = await sql<{ id: string }[]>` SELECT id FROM sessions WHERE id = ${sessionId} `; if (sessionRows.length === 0) { reply.code(404); return { error: 'session not found' }; } const chatId = await resolveChatId(sql, sessionId, pane_id); if (!chatId) { reply.code(404); return { error: 'pane not found' }; } if (inference.hasActive(chatId)) { reply.code(409); return { error: 'inference already running on this chat' }; } const userText = parsed.data.user_message?.trim() ? parsed.data.user_message : DEFAULT_SKILL_USER_MESSAGE; const body = await getSkillBody(skill_name); if (body === null) { reply.code(404); return { error: 'unknown_skill', message: `unknown skill: ${skill_name}` }; } const { result, toolCall } = await runSkillInvokeTransaction(sql, { sessionId, chatId, skillName: skill_name, skillBody: body, userText, }); for (const frame of buildSkillInvokeSyntheticFrames(chatId, result, toolCall, body)) { broker.publishFrame(sessionId, frame as WsFrame); } for (const frame of buildSkillInvokeUserFrames(chatId, result.user_message_id, userText)) { broker.publishFrame(sessionId, frame as WsFrame); } inference.enqueue(sessionId, chatId, result.assistant_message_id, 'default'); reply.code(202); return result; }, ); }