diff --git a/src/gateway/handlers/agent.ts b/src/gateway/handlers/agent.ts index 4255a1b..4f08138 100644 --- a/src/gateway/handlers/agent.ts +++ b/src/gateway/handlers/agent.ts @@ -1,7 +1,8 @@ -import type { GatewayRequest, OutboundMessage } from '../protocol.js'; +import type { GatewayRequest, GatewayAttachment, OutboundMessage } from '../protocol.js'; import type { SendFn } from '../router.js'; import { makeEvent, makeError, ErrorCode } from '../protocol.js'; import type { SessionBridge } from '../session-bridge.js'; +import type { Attachment } from '../../channels/types.js'; export interface AgentHandlerDeps { sessionBridge: SessionBridge; @@ -10,7 +11,7 @@ export interface AgentHandlerDeps { export function createAgentHandlers(deps: AgentHandlerDeps) { return { 'agent.send': async (request: GatewayRequest, send: SendFn): Promise => { - const params = request.params as { message?: string; connectionId?: string } | undefined; + const params = request.params as { message?: string; connectionId?: string; attachments?: GatewayAttachment[] } | undefined; if (!params?.message) { return makeError(request.id, ErrorCode.InvalidRequest, 'message is required'); } @@ -48,7 +49,15 @@ export function createAgentHandlers(deps: AgentHandlerDeps) { }); try { - const response = await agent.process(params.message); + // Convert gateway attachments to channel attachments + const attachments: Attachment[] | undefined = params.attachments?.map(a => ({ + mimeType: a.mimeType, + data: a.data, + url: a.url, + filename: a.filename, + })); + + const response = await agent.process(params.message, attachments); send(makeEvent(request.id, 'done', { content: response })); } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; diff --git a/src/gateway/handlers/handlers.test.ts b/src/gateway/handlers/handlers.test.ts index 4c4eac3..a2ed374 100644 --- a/src/gateway/handlers/handlers.test.ts +++ b/src/gateway/handlers/handlers.test.ts @@ -206,13 +206,51 @@ describe('agent handlers', () => { await handlers['agent.send'](req, send); - expect(mockAgent.process).toHaveBeenCalledWith('hello'); + expect(mockAgent.process).toHaveBeenCalledWith('hello', undefined); expect(sent).toHaveLength(1); const doneEvent = sent[0] as GatewayEvent; expect(doneEvent.event).toBe('done'); expect((doneEvent.data as any).content).toBe('response text'); }); + it('agent.send passes attachments to agent.process', async () => { + const attachments = [ + { mimeType: 'image/png', data: 'iVBOR...', filename: 'screenshot.png' }, + { mimeType: 'application/pdf', url: 'https://example.com/doc.pdf' }, + ]; + const req: GatewayRequest = { + id: 10, + method: 'agent.send', + params: { message: 'describe this', connectionId: 'conn-1', attachments }, + }; + const sent: OutboundMessage[] = []; + const send = vi.fn((msg: OutboundMessage) => sent.push(msg)); + + await handlers['agent.send'](req, send); + + expect(mockAgent.process).toHaveBeenCalledWith('describe this', [ + { mimeType: 'image/png', data: 'iVBOR...', url: undefined, filename: 'screenshot.png' }, + { mimeType: 'application/pdf', data: undefined, url: 'https://example.com/doc.pdf', filename: undefined }, + ]); + const doneEvent = sent[0] as GatewayEvent; + expect(doneEvent.event).toBe('done'); + }); + + it('agent.send works with empty attachments array', async () => { + const req: GatewayRequest = { + id: 11, + method: 'agent.send', + params: { message: 'hi', connectionId: 'conn-1', attachments: [] }, + }; + const sent: OutboundMessage[] = []; + const send = vi.fn((msg: OutboundMessage) => sent.push(msg)); + + await handlers['agent.send'](req, send); + + expect(mockAgent.process).toHaveBeenCalledWith('hi', []); + expect(sent).toHaveLength(1); + }); + it('agent.send requires message', async () => { const req: GatewayRequest = { id: 2, method: 'agent.send', params: { connectionId: 'conn-1' } }; const send = vi.fn(); diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 2236b25..1a0235c 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -20,11 +20,13 @@ export type { GatewayResponse, GatewayError, GatewayEvent, + GatewayAttachment, OutboundMessage, EventType, ContentEventData, ToolStartEventData, ToolEndEventData, + AttachmentEventData, DoneEventData, ErrorEventData, } from './protocol.js'; diff --git a/src/gateway/protocol.test.ts b/src/gateway/protocol.test.ts index 4206d04..6ba4198 100644 --- a/src/gateway/protocol.test.ts +++ b/src/gateway/protocol.test.ts @@ -86,5 +86,23 @@ describe('protocol', () => { data: { text: 'hello' }, }); }); + + it('creates an attachment event message', () => { + const data = { mimeType: 'image/png', data: 'iVBOR...', filename: 'screenshot.png' }; + expect(makeEvent(1, 'attachment', data)).toEqual({ + id: 1, + event: 'attachment', + data, + }); + }); + + it('creates an attachment event with url', () => { + const data = { mimeType: 'application/pdf', url: 'https://example.com/doc.pdf', filename: 'doc.pdf' }; + expect(makeEvent(2, 'attachment', data)).toEqual({ + id: 2, + event: 'attachment', + data, + }); + }); }); }); diff --git a/src/gateway/protocol.ts b/src/gateway/protocol.ts index 40374fa..15c38b6 100644 --- a/src/gateway/protocol.ts +++ b/src/gateway/protocol.ts @@ -29,12 +29,27 @@ export interface GatewayEvent { data: unknown; } +// ── Attachment data for gateway protocol messages ─────────────── + +/** Attachment data sent in agent.send params or emitted as events. */ +export interface GatewayAttachment { + /** MIME type (e.g. "image/jpeg", "audio/ogg") */ + mimeType: string; + /** Base64-encoded data */ + data?: string; + /** URL to the resource */ + url?: string; + /** Filename hint */ + filename?: string; +} + // ── Event types emitted during agent.send ────────────────────── export type EventType = | 'content' | 'tool_start' | 'tool_end' + | 'attachment' | 'done' | 'error'; @@ -56,6 +71,13 @@ export interface ToolEndEventData { }; } +export interface AttachmentEventData { + mimeType: string; + data?: string; + url?: string; + filename?: string; +} + export interface DoneEventData { content: string; }