Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import { z } from 'zod';
|
|
import type { Sql } from '../db.js';
|
|
import type { Chat } from '../types/api.js';
|
|
import { getSkillBody, listSkills } from '../services/skills.js';
|
|
import {
|
|
buildSkillInvokeSyntheticFrames,
|
|
DEFAULT_SKILL_USER_MESSAGE,
|
|
runSkillInvokeTransaction,
|
|
} from '../services/skill-invoke.js';
|
|
|
|
// Batch 9.6 slash-invoke handlers. Mirrors the MessageHandlers shape in
|
|
// routes/messages.ts so index.ts can pass thin adapters around broker +
|
|
// inference runner without skills.ts importing them directly.
|
|
export interface SkillInvokeHandlers {
|
|
enqueueInference: (
|
|
sessionId: string,
|
|
chatId: string,
|
|
assistantMessageId: string,
|
|
user: string,
|
|
) => void;
|
|
publishUserMessage: (
|
|
sessionId: string,
|
|
chatId: string,
|
|
userMessageId: string,
|
|
content: string,
|
|
) => void;
|
|
publishSessionFrame: (
|
|
sessionId: string,
|
|
frame: Record<string, unknown> & { type: string },
|
|
) => void;
|
|
}
|
|
|
|
const SkillInvokeBody = z.object({
|
|
skill_name: z.string().min(1),
|
|
// Optional — server fills in a default if absent or whitespace-only so the
|
|
// model always has something to act on (matches the spec's "Apply this
|
|
// skill." filler).
|
|
user_message: z.string().max(64_000).nullable().optional(),
|
|
});
|
|
|
|
export function registerSkillsRoutes(
|
|
app: FastifyInstance,
|
|
sql: Sql,
|
|
handlers: SkillInvokeHandlers,
|
|
): void {
|
|
// Debug/admin surface — the model interacts with skills via the three
|
|
// skill_* tools, not through this endpoint.
|
|
app.get('/api/skills', async () => {
|
|
return { skills: await listSkills() };
|
|
});
|
|
|
|
// POST /api/chats/:id/skill_invoke — slash-command entry point. Loads the
|
|
// skill body server-side (clients never get to forge file content),
|
|
// persists 4 messages in one transaction (synthetic assistant tool_use,
|
|
// synthetic tool result, real user message, streaming assistant), and
|
|
// enqueues inference against the updated history.
|
|
app.post<{ Params: { id: string } }>(
|
|
'/api/chats/:id/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 { skill_name } = parsed.data;
|
|
const userText = parsed.data.user_message?.trim()
|
|
? parsed.data.user_message
|
|
: DEFAULT_SKILL_USER_MESSAGE;
|
|
|
|
const chatRows = await sql<Chat[]>`
|
|
SELECT id, session_id FROM chats WHERE id = ${req.params.id} AND status = 'open'
|
|
`;
|
|
if (chatRows.length === 0) {
|
|
reply.code(404);
|
|
return { error: 'chat not found' };
|
|
}
|
|
const chat = chatRows[0]!;
|
|
const sessionId = chat.session_id;
|
|
|
|
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: chat.id,
|
|
skillName: skill_name,
|
|
skillBody: body,
|
|
userText,
|
|
});
|
|
|
|
// Synthetic frames so useSessionStream's reducer reflects the new
|
|
// history without a refetch. Frame shapes match the streaming-inference
|
|
// protocol (see services/inference.ts InferenceFrame).
|
|
for (const frame of buildSkillInvokeSyntheticFrames(chat.id, result, toolCall, body)) {
|
|
handlers.publishSessionFrame(sessionId, frame);
|
|
}
|
|
handlers.publishUserMessage(sessionId, chat.id, result.user_message_id, userText);
|
|
handlers.enqueueInference(sessionId, chat.id, result.assistant_message_id, 'default');
|
|
|
|
reply.code(202);
|
|
return result;
|
|
},
|
|
);
|
|
}
|