v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes
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>
This commit is contained in:
93
apps/coder/src/routes/skills.ts
Normal file
93
apps/coder/src/routes/skills.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
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;
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user