Add canvas artifact RPC foundation for A2UI
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
|
||||
import { makeError, makeResponse, ErrorCode } from '../protocol.js';
|
||||
import type { CanvasStore } from '../canvas-store.js';
|
||||
|
||||
export interface CanvasHandlerDeps {
|
||||
store: CanvasStore;
|
||||
}
|
||||
|
||||
export function createCanvasHandlers(deps: CanvasHandlerDeps) {
|
||||
return {
|
||||
'canvas.list': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as { sessionId?: string } | undefined;
|
||||
if (!params?.sessionId) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'sessionId is required');
|
||||
}
|
||||
|
||||
const artifacts = deps.store.list(params.sessionId);
|
||||
return makeResponse(request.id, { artifacts });
|
||||
},
|
||||
|
||||
'canvas.get': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as { sessionId?: string; artifactId?: string } | undefined;
|
||||
if (!params?.sessionId) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'sessionId is required');
|
||||
}
|
||||
if (!params?.artifactId) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'artifactId is required');
|
||||
}
|
||||
|
||||
const artifact = deps.store.get(params.sessionId, params.artifactId);
|
||||
if (!artifact) {
|
||||
return makeError(request.id, ErrorCode.SessionNotFound, 'Canvas artifact not found');
|
||||
}
|
||||
return makeResponse(request.id, { artifact });
|
||||
},
|
||||
|
||||
'canvas.put': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as {
|
||||
sessionId?: string;
|
||||
artifactId?: string;
|
||||
type?: string;
|
||||
title?: string;
|
||||
content?: unknown;
|
||||
metadata?: Record<string, unknown>;
|
||||
} | undefined;
|
||||
if (!params?.sessionId) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'sessionId is required');
|
||||
}
|
||||
if (!params?.type || typeof params.type !== 'string' || !params.type.trim()) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'type is required');
|
||||
}
|
||||
if (params.content === undefined) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'content is required');
|
||||
}
|
||||
|
||||
const artifact = deps.store.put(params.sessionId, {
|
||||
id: params.artifactId,
|
||||
type: params.type,
|
||||
title: params.title,
|
||||
content: params.content,
|
||||
metadata: params.metadata,
|
||||
});
|
||||
return makeResponse(request.id, { artifact, upserted: true });
|
||||
},
|
||||
|
||||
'canvas.delete': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as { sessionId?: string; artifactId?: string } | undefined;
|
||||
if (!params?.sessionId) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'sessionId is required');
|
||||
}
|
||||
if (!params?.artifactId) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'artifactId is required');
|
||||
}
|
||||
|
||||
const deleted = deps.store.delete(params.sessionId, params.artifactId);
|
||||
return makeResponse(request.id, { deleted });
|
||||
},
|
||||
|
||||
'canvas.clear': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as { sessionId?: string } | undefined;
|
||||
if (!params?.sessionId) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'sessionId is required');
|
||||
}
|
||||
|
||||
const cleared = deps.store.clear(params.sessionId);
|
||||
return makeResponse(request.id, { cleared });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -7,10 +7,12 @@ import { createAgentHandlers } from './agent.js';
|
||||
import { createIntentHandlers } from './intents.js';
|
||||
import { createRoutingHandlers } from './routing.js';
|
||||
import { createHistoryHandlers } from './history.js';
|
||||
import { createCanvasHandlers } from './canvas.js';
|
||||
import { createConfigHandlers, redactConfig } from './config.js';
|
||||
import { createPairingHandlers } from './pairing.js';
|
||||
import { PairingManager } from '../../channels/pairing.js';
|
||||
import { LaneQueue } from '../lane-queue.js';
|
||||
import { CanvasStore } from '../canvas-store.js';
|
||||
import { ErrorCode } from '../protocol.js';
|
||||
import type { GatewayRequest, GatewayResponse, GatewayError, GatewayEvent, OutboundMessage } from '../protocol.js';
|
||||
import { ComponentRegistry } from '../../intents/index.js';
|
||||
@@ -350,6 +352,82 @@ describe('session handlers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('canvas handlers', () => {
|
||||
it('supports put/get/list/delete/clear lifecycle', async () => {
|
||||
const handlers = createCanvasHandlers({ store: new CanvasStore() });
|
||||
|
||||
const putReq: GatewayRequest = {
|
||||
id: 1,
|
||||
method: 'canvas.put',
|
||||
params: {
|
||||
sessionId: 'ws:abc',
|
||||
artifactId: 'card-1',
|
||||
type: 'note',
|
||||
title: 'Draft',
|
||||
content: { text: 'hello' },
|
||||
},
|
||||
};
|
||||
const putRes = await handlers['canvas.put'](putReq) as GatewayResponse;
|
||||
expect(getPath(putRes.result, 'artifact', 'id')).toBe('card-1');
|
||||
|
||||
const getRes = await handlers['canvas.get']({
|
||||
id: 2,
|
||||
method: 'canvas.get',
|
||||
params: { sessionId: 'ws:abc', artifactId: 'card-1' },
|
||||
}) as GatewayResponse;
|
||||
expect(getPath(getRes.result, 'artifact', 'title')).toBe('Draft');
|
||||
|
||||
const listRes = await handlers['canvas.list']({
|
||||
id: 3,
|
||||
method: 'canvas.list',
|
||||
params: { sessionId: 'ws:abc' },
|
||||
}) as GatewayResponse;
|
||||
expect((getPath(listRes.result, 'artifacts') as unknown[]).length).toBe(1);
|
||||
|
||||
const delRes = await handlers['canvas.delete']({
|
||||
id: 4,
|
||||
method: 'canvas.delete',
|
||||
params: { sessionId: 'ws:abc', artifactId: 'card-1' },
|
||||
}) as GatewayResponse;
|
||||
expect(getPath(delRes.result, 'deleted')).toBe(true);
|
||||
|
||||
await handlers['canvas.put']({
|
||||
id: 5,
|
||||
method: 'canvas.put',
|
||||
params: { sessionId: 'ws:abc', artifactId: 'card-2', type: 'note', content: 'a' },
|
||||
});
|
||||
await handlers['canvas.put']({
|
||||
id: 6,
|
||||
method: 'canvas.put',
|
||||
params: { sessionId: 'ws:abc', artifactId: 'card-3', type: 'note', content: 'b' },
|
||||
});
|
||||
const clearRes = await handlers['canvas.clear']({
|
||||
id: 7,
|
||||
method: 'canvas.clear',
|
||||
params: { sessionId: 'ws:abc' },
|
||||
}) as GatewayResponse;
|
||||
expect(getPath(clearRes.result, 'cleared')).toBe(2);
|
||||
});
|
||||
|
||||
it('validates required params', async () => {
|
||||
const handlers = createCanvasHandlers({ store: new CanvasStore() });
|
||||
|
||||
const missingSession = await handlers['canvas.list']({
|
||||
id: 8,
|
||||
method: 'canvas.list',
|
||||
params: {},
|
||||
}) as GatewayError;
|
||||
expect(missingSession.error.code).toBe(ErrorCode.InvalidRequest);
|
||||
|
||||
const missingContent = await handlers['canvas.put']({
|
||||
id: 9,
|
||||
method: 'canvas.put',
|
||||
params: { sessionId: 'ws:abc', type: 'note' },
|
||||
}) as GatewayError;
|
||||
expect(missingContent.error.code).toBe(ErrorCode.InvalidRequest);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool handlers', () => {
|
||||
const mockTool = {
|
||||
name: 'test.tool',
|
||||
|
||||
@@ -17,5 +17,7 @@ export { createRoutingHandlers } from './routing.js';
|
||||
export type { RoutingHandlerDeps } from './routing.js';
|
||||
export { createHistoryHandlers } from './history.js';
|
||||
export type { HistoryHandlerDeps } from './history.js';
|
||||
export { createCanvasHandlers } from './canvas.js';
|
||||
export type { CanvasHandlerDeps } from './canvas.js';
|
||||
export { createNodeHandlers } from './node.js';
|
||||
export type { NodeHandlerDeps, NodeRegistration, NodeConnectionState, NodeLocation } from './node.js';
|
||||
|
||||
Reference in New Issue
Block a user