From 96726377fdb4a08388f9e058db0492aae604409d Mon Sep 17 00:00:00 2001 From: User Date: Thu, 21 May 2026 16:18:03 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20code=20quality=20cleanup=20?= =?UTF-8?q?=E2=80=94=20DRY=20schemas,=20consolidate=20dispatch,=20remove?= =?UTF-8?q?=20dead=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add createResponseSchema() factory to eliminate ~26 repeated z.union() blocks - Extract shared dispatchNotification() from duplicated protocol/client logic - Remove orphaned aggregateMessages() and isPartialMessage() (knip-verified) - Fix misleading @deprecated on LEGACY_FACTORY_API_VERSION (it's required by wire protocol) - Remove ~70 name-restating JSDoc comments that add no information beyond type names - Remove stale internal monorepo path references from enums.ts header - Add 11 tests for createResponseSchema and dispatchNotification Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/client.ts | 40 +---- src/index.ts | 3 +- src/protocol.ts | 54 ++++--- src/schemas/client.ts | 308 ++++++++++--------------------------- src/schemas/constants.ts | 5 +- src/schemas/enums.ts | 21 --- src/schemas/index.ts | 1 + src/schemas/mcp.ts | 8 - src/schemas/server.ts | 51 +----- src/schemas/shared.ts | 8 + src/session.ts | 33 ---- src/stream.ts | 11 -- tests/code-quality.test.ts | 199 ++++++++++++++++++++++++ 13 files changed, 336 insertions(+), 406 deletions(-) create mode 100644 tests/code-quality.test.ts diff --git a/src/client.ts b/src/client.ts index 42fdf05..7ef4b38 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2,10 +2,12 @@ import type { z } from 'zod'; import { ConnectionError, SessionError } from './errors.js'; import { + dispatchNotification, ProtocolEngine, type AskUserHandler, type NotificationCallback, type NotificationFilter, + type NotificationListener, type PermissionHandler, } from './protocol.js'; import type { @@ -93,7 +95,6 @@ import { SESSION_INIT_TIMEOUT, } from './schemas/constants.js'; import { DroidServerMethod, ToolConfirmationOutcome } from './schemas/enums.js'; -import { SessionNotificationParamsSchema } from './schemas/server.js'; import type { AskUserRequestParams, AskUserResult, @@ -106,11 +107,6 @@ export type ClientPermissionHandler = PermissionHandler; export type ClientAskUserHandler = AskUserHandler; -interface ClientNotificationListener { - readonly callback: NotificationCallback; - readonly filter?: NotificationFilter; -} - export interface DroidClientOptions { /** A connected DroidClientTransport implementation. */ transport: DroidClientTransport; @@ -124,11 +120,7 @@ export class DroidClient { private _sessionId: string | null = null; private _closed = false; - /** - * Client-level notification listeners. - * Each entry is [callback, optional notification filter]. - */ - private readonly _notificationListeners: ClientNotificationListener[] = []; + private readonly _notificationListeners: NotificationListener[] = []; /** Client-level permission handler. */ private _permissionHandler: ClientPermissionHandler | null = null; @@ -463,7 +455,7 @@ export class DroidClient { callback: NotificationCallback, filter?: NotificationFilter ): () => void { - const entry: ClientNotificationListener = { callback, filter }; + const entry: NotificationListener = { callback, filter }; this._notificationListeners.push(entry); let unsubscribed = false; @@ -508,29 +500,7 @@ export class DroidClient { } private _dispatchNotification(notification: Record): void { - let notificationType: string | undefined; - const parsed = SessionNotificationParamsSchema.safeParse( - notification['params'] - ); - if (parsed.success) { - notificationType = parsed.data.notification.type; - } - - const listeners = [...this._notificationListeners]; - for (const listener of listeners) { - if ( - listener.filter?.type != null && - listener.filter.type !== notificationType - ) { - continue; - } - - try { - listener.callback(notification); - } catch { - // Notification listener raised — don't crash the client - } - } + dispatchNotification(notification, [...this._notificationListeners]); } private _dispatchPermissionRequest( diff --git a/src/index.ts b/src/index.ts index acf2ae4..008b6f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,11 +7,12 @@ export * from './errors.js'; export * from './types.js'; export { ProcessTransport } from './transport.js'; -export { ProtocolEngine } from './protocol.js'; +export { dispatchNotification, ProtocolEngine } from './protocol.js'; export type { AskUserHandler, NotificationCallback, NotificationFilter, + NotificationListener, PermissionHandler, } from './protocol.js'; diff --git a/src/protocol.ts b/src/protocol.ts index 6dc35f3..edb1f9d 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -60,11 +60,40 @@ interface PendingRequest { readonly timer: ReturnType; } -interface NotificationListener { +export interface NotificationListener { readonly callback: NotificationCallback; readonly filter?: NotificationFilter; } +/** Dispatch a notification to matching listeners, swallowing listener errors. */ +export function dispatchNotification( + notification: Record, + listeners: Iterable +): void { + let notificationType: string | undefined; + const parsed = SessionNotificationParamsSchema.safeParse( + notification['params'] + ); + if (parsed.success) { + notificationType = parsed.data.notification.type; + } + + for (const listener of listeners) { + if ( + listener.filter?.type != null && + listener.filter.type !== notificationType + ) { + continue; + } + + try { + listener.callback(notification); + } catch { + // Notification listener raised — don't crash the dispatch loop + } + } +} + export class ProtocolEngine { private readonly _transport: DroidClientTransport; private readonly _defaultTimeout: number; @@ -277,28 +306,7 @@ export class ProtocolEngine { } private _handleNotification(notification: Record): void { - let notificationType: string | undefined; - const parsed = SessionNotificationParamsSchema.safeParse( - notification['params'] - ); - if (parsed.success) { - notificationType = parsed.data.notification.type; - } - - for (const listener of this._notificationListeners) { - if ( - listener.filter?.type != null && - listener.filter.type !== notificationType - ) { - continue; - } - - try { - listener.callback(notification); - } catch { - // Notification listener raised — don't crash the engine - } - } + dispatchNotification(notification, this._notificationListeners); } private async _handleServerRequest( diff --git a/src/schemas/client.ts b/src/schemas/client.ts index 00c58c6..3d8014b 100644 --- a/src/schemas/client.ts +++ b/src/schemas/client.ts @@ -23,11 +23,11 @@ import { import { Base64ImageSourceSchema, DocumentSourceSchema } from './messages.js'; import { MissionFeatureSchema, ProgressLogEntrySchema } from './mission.js'; import { + createResponseSchema, EmptyResultSchema, JsonObjectSchema, type JsonRpcResponseFailure, JsonRpcRequestSchema, - JsonRpcResponseFailureSchema, JsonRpcResponseSuccessSchema, SuccessResultSchema, ToolSelectionOverridesSchema, @@ -40,7 +40,6 @@ const SessionModeRequestFieldsShape = { autonomyLevel: z.nativeEnum(AutonomyLevel).optional(), }; -/** Session tag metadata. */ export const SessionTagSchema = z .object({ name: z.string(), @@ -50,7 +49,6 @@ export const SessionTagSchema = z export type SessionTag = z.infer; -/** Session source information. */ export const SessionSourceSchema = z .object({ platform: z.string(), @@ -59,7 +57,6 @@ export const SessionSourceSchema = z export type SessionSource = z.infer; -/** Stdio MCP server configuration for initialization. */ export const StdioMcpConfigSchema = z .object({ name: z.string(), @@ -71,7 +68,6 @@ export const StdioMcpConfigSchema = z export type StdioMcpConfig = z.infer; -/** HTTP header for MCP server configuration. */ export const HttpHeaderSchema = z .object({ name: z.string(), @@ -81,7 +77,6 @@ export const HttpHeaderSchema = z export type HttpHeader = z.infer; -/** HTTP MCP server configuration for initialization. */ export const HttpMcpConfigSchema = z .object({ type: z.literal('http'), @@ -93,7 +88,6 @@ export const HttpMcpConfigSchema = z export type HttpMcpConfig = z.infer; -/** SSE MCP server configuration for initialization. */ export const SseMcpConfigSchema = z .object({ type: z.literal('sse'), @@ -105,7 +99,6 @@ export const SseMcpConfigSchema = z export type SseMcpConfig = z.infer; -/** Union of MCP server configs. */ export const McpServerConfigSchema = z.union([ StdioMcpConfigSchema, HttpMcpConfigSchema, @@ -133,7 +126,6 @@ export const SessionSettingsSchema = z export type SessionSettings = z.infer; -/** Git repository information. */ export const GitRepoInfoSchema = z .object({ owner: z.string().optional(), @@ -166,7 +158,6 @@ export const AvailableModelConfigSchema = z export type AvailableModelConfig = z.infer; -/** Token usage information for a session. */ export const TokenUsageSchema = z .object({ inputTokens: z.number(), @@ -192,7 +183,6 @@ export const ContextStatsSchema = z export type ContextStats = z.infer; -/** Worker state information for mission snapshots. */ export const WorkerStateInfoSchema = z .object({ startedAt: z.string(), @@ -203,7 +193,6 @@ export const WorkerStateInfoSchema = z export type WorkerStateInfo = z.infer; -/** Skill resource file in a skill folder. */ export const SkillResourceSchema = z .object({ name: z.string(), @@ -214,7 +203,6 @@ export const SkillResourceSchema = z export type SkillResource = z.infer; -/** Skill information returned by list_skills. */ export const SkillInfoSchema = z .object({ name: z.string(), @@ -231,7 +219,6 @@ export const SkillInfoSchema = z export type SkillInfo = z.infer; -/** Parameters for droid.initialize_session request. */ export const InitializeSessionRequestParamsSchema = z .object({ machineId: z.string(), @@ -258,7 +245,6 @@ export type InitializeSessionRequestParams = z.infer< typeof InitializeSessionRequestParamsSchema >; -/** Parameters for droid.load_session request. */ export const LoadSessionRequestParamsSchema = z .object({ sessionId: z.string(), @@ -271,7 +257,6 @@ export type LoadSessionRequestParams = z.infer< typeof LoadSessionRequestParamsSchema >; -/** Structured output format type names. */ export const OutputFormatType = { JsonSchema: 'json_schema', } as const; @@ -279,7 +264,6 @@ export const OutputFormatType = { export type OutputFormatType = (typeof OutputFormatType)[keyof typeof OutputFormatType]; -/** Structured output format for droid.add_user_message request. */ export const OutputFormatSchema = z .object({ type: z.literal(OutputFormatType.JsonSchema), @@ -289,7 +273,6 @@ export const OutputFormatSchema = z export type OutputFormat = z.infer; -/** Parameters for droid.add_user_message request. */ export const AddUserMessageRequestParamsSchema = z .object({ messageId: z.string().optional(), @@ -304,14 +287,12 @@ export type AddUserMessageRequestParams = z.infer< typeof AddUserMessageRequestParamsSchema >; -/** Parameters for droid.interrupt_session request (empty). */ export const InterruptSessionRequestParamsSchema = z.object({}).strict(); export type InterruptSessionRequestParams = z.infer< typeof InterruptSessionRequestParamsSchema >; -/** Parameters for droid.close_session request. */ export const CloseSessionRequestParamsSchema = z .object({ reason: z @@ -324,7 +305,6 @@ export type CloseSessionRequestParams = z.infer< typeof CloseSessionRequestParamsSchema >; -/** Parameters for droid.kill_worker_session request. */ export const KillWorkerSessionRequestParamsSchema = z .object({ workerSessionId: z.string(), @@ -335,7 +315,6 @@ export type KillWorkerSessionRequestParams = z.infer< typeof KillWorkerSessionRequestParamsSchema >; -/** Parameters for droid.update_session_settings request. */ export const UpdateSessionSettingsRequestParamsSchema = z .object({ ...SessionModeRequestFieldsShape, @@ -353,7 +332,6 @@ export type UpdateSessionSettingsRequestParams = z.infer< typeof UpdateSessionSettingsRequestParamsSchema >; -/** Parameters for droid.toggle_mcp_server request. */ export const ToggleMcpServerRequestParamsSchema = z .object({ serverName: z.string(), @@ -366,7 +344,6 @@ export type ToggleMcpServerRequestParams = z.infer< typeof ToggleMcpServerRequestParamsSchema >; -/** Parameters for droid.authenticate_mcp_server request. */ export const AuthenticateMcpServerRequestParamsSchema = z .object({ serverName: z.string(), @@ -377,7 +354,6 @@ export type AuthenticateMcpServerRequestParams = z.infer< typeof AuthenticateMcpServerRequestParamsSchema >; -/** Parameters for droid.cancel_mcp_auth request. */ export const CancelMcpAuthRequestParamsSchema = z .object({ serverName: z.string(), @@ -388,7 +364,6 @@ export type CancelMcpAuthRequestParams = z.infer< typeof CancelMcpAuthRequestParamsSchema >; -/** Parameters for droid.clear_mcp_auth request. */ export const ClearMcpAuthRequestParamsSchema = z .object({ serverName: z.string(), @@ -399,7 +374,6 @@ export type ClearMcpAuthRequestParams = z.infer< typeof ClearMcpAuthRequestParamsSchema >; -/** Parameters for droid.submit_mcp_auth_code request. */ export const SubmitMcpAuthCodeRequestParamsSchema = z .object({ serverName: z.string(), @@ -412,7 +386,6 @@ export type SubmitMcpAuthCodeRequestParams = z.infer< typeof SubmitMcpAuthCodeRequestParamsSchema >; -/** Parameters for droid.add_mcp_server request. */ export const AddMcpServerRequestParamsSchema = z .object({ name: z.string(), @@ -429,7 +402,6 @@ export type AddMcpServerRequestParams = z.infer< typeof AddMcpServerRequestParamsSchema >; -/** Parameters for droid.remove_mcp_server request. */ export const RemoveMcpServerRequestParamsSchema = z .object({ serverName: z.string(), @@ -441,21 +413,18 @@ export type RemoveMcpServerRequestParams = z.infer< typeof RemoveMcpServerRequestParamsSchema >; -/** Parameters for droid.list_mcp_registry request (empty). */ export const ListMcpRegistryRequestParamsSchema = z.object({}).strict(); export type ListMcpRegistryRequestParams = z.infer< typeof ListMcpRegistryRequestParamsSchema >; -/** Parameters for droid.list_mcp_tools request (empty). */ export const ListMcpToolsRequestParamsSchema = z.object({}).strict(); export type ListMcpToolsRequestParams = z.infer< typeof ListMcpToolsRequestParamsSchema >; -/** Tool catalog entry returned by droid.list_tools. */ export const ExecToolInfoSchema = z .object({ id: z.string(), @@ -470,7 +439,6 @@ export const ExecToolInfoSchema = z export type ExecToolInfo = z.infer; -/** Parameters for droid.list_tools. */ export const ListToolsRequestParamsSchema = z .object({ ...SessionModeRequestFieldsShape, @@ -485,14 +453,12 @@ export type ListToolsRequestParams = z.infer< typeof ListToolsRequestParamsSchema >; -/** Parameters for droid.list_mcp_servers request (empty). */ export const ListMcpServersRequestParamsSchema = z.object({}).strict(); export type ListMcpServersRequestParams = z.infer< typeof ListMcpServersRequestParamsSchema >; -/** Parameters for droid.toggle_mcp_tool request. */ export const ToggleMcpToolRequestParamsSchema = z .object({ serverName: z.string(), @@ -505,14 +471,12 @@ export type ToggleMcpToolRequestParams = z.infer< typeof ToggleMcpToolRequestParamsSchema >; -/** Parameters for droid.list_skills request (empty). */ export const ListSkillsRequestParamsSchema = z.object({}).strict(); export type ListSkillsRequestParams = z.infer< typeof ListSkillsRequestParamsSchema >; -/** Parameters for droid.submit_bug_report request. */ export const SubmitBugReportRequestParamsSchema = z .object({ userComment: z.string(), @@ -524,7 +488,6 @@ export type SubmitBugReportRequestParams = z.infer< typeof SubmitBugReportRequestParamsSchema >; -/** File snapshot for rewind operations. */ export const RewindFileSnapshotSchema = z .object({ filePath: z.string(), @@ -535,7 +498,6 @@ export const RewindFileSnapshotSchema = z export type RewindFileSnapshot = z.infer; -/** File creation record for rewind operations. */ export const RewindFileCreationSchema = z .object({ filePath: z.string(), @@ -544,7 +506,6 @@ export const RewindFileCreationSchema = z export type RewindFileCreation = z.infer; -/** Evicted file record for rewind operations. */ export const RewindEvictedFileSchema = z .object({ filePath: z.string(), @@ -554,7 +515,6 @@ export const RewindEvictedFileSchema = z export type RewindEvictedFile = z.infer; -/** Parameters for droid.get_rewind_info request. */ export const GetRewindInfoRequestParamsSchema = z .object({ messageId: z.string(), @@ -565,7 +525,6 @@ export type GetRewindInfoRequestParams = z.infer< typeof GetRewindInfoRequestParamsSchema >; -/** Parameters for droid.execute_rewind request. */ export const ExecuteRewindRequestParamsSchema = z .object({ messageId: z.string(), @@ -579,7 +538,6 @@ export type ExecuteRewindRequestParams = z.infer< typeof ExecuteRewindRequestParamsSchema >; -/** Parameters for droid.compact_session request. */ export const CompactSessionRequestParamsSchema = z .object({ customInstructions: z.string().optional(), @@ -590,7 +548,6 @@ export type CompactSessionRequestParams = z.infer< typeof CompactSessionRequestParamsSchema >; -/** Parameters for droid.rename_session request. */ export const RenameSessionRequestParamsSchema = z .object({ title: z.string(), @@ -601,14 +558,12 @@ export type RenameSessionRequestParams = z.infer< typeof RenameSessionRequestParamsSchema >; -/** Parameters for droid.fork_session request (empty). */ export const ForkSessionRequestParamsSchema = z.object({}).passthrough(); export type ForkSessionRequestParams = z.infer< typeof ForkSessionRequestParamsSchema >; -/** Parameters for droid.get_context_stats request (empty). */ export const GetContextStatsRequestParamsSchema = z.object({}).passthrough(); export type GetContextStatsRequestParams = z.infer< @@ -859,7 +814,6 @@ export const ClientRequestSchema = z.discriminatedUnion('method', [ export type ClientRequest = z.infer; -/** Mission state snapshot (for orchestrator sessions). */ export const MissionSnapshotSchema = z .object({ state: z.nativeEnum(MissionState), @@ -874,7 +828,6 @@ export const MissionSnapshotSchema = z export type MissionSnapshot = z.infer; -/** Result for droid.initialize_session response. */ export const InitializeSessionResultSchema = z .object({ sessionId: z.string(), @@ -890,7 +843,6 @@ export type InitializeSessionResult = z.infer< typeof InitializeSessionResultSchema >; -/** Result for droid.load_session response. */ export const LoadSessionResultSchema = z .object({ session: JsonObjectSchema, @@ -913,98 +865,82 @@ export const LoadSessionResultSchema = z export type LoadSessionResult = z.infer; -/** Result for droid.add_user_message response (empty). */ export const AddUserMessageResultSchema = EmptyResultSchema; export type AddUserMessageResult = z.infer; -/** Result for droid.interrupt_session response (empty). */ export const InterruptSessionResultSchema = EmptyResultSchema; export type InterruptSessionResult = z.infer< typeof InterruptSessionResultSchema >; -/** Result for droid.close_session response (empty). */ export const CloseSessionResultSchema = EmptyResultSchema; export type CloseSessionResult = z.infer; -/** Result for droid.kill_worker_session response (empty). */ export const KillWorkerSessionResultSchema = EmptyResultSchema; export type KillWorkerSessionResult = z.infer< typeof KillWorkerSessionResultSchema >; -/** Result for droid.update_session_settings response (empty). */ export const UpdateSessionSettingsResultSchema = EmptyResultSchema; export type UpdateSessionSettingsResult = z.infer< typeof UpdateSessionSettingsResultSchema >; -/** Result for droid.toggle_mcp_server response. */ export const ToggleMcpServerResultSchema = SuccessResultSchema; export type ToggleMcpServerResult = z.infer; -/** Result for droid.authenticate_mcp_server response. */ export const AuthenticateMcpServerResultSchema = SuccessResultSchema; export type AuthenticateMcpServerResult = z.infer< typeof AuthenticateMcpServerResultSchema >; -/** Result for droid.cancel_mcp_auth response. */ export const CancelMcpAuthResultSchema = SuccessResultSchema; export type CancelMcpAuthResult = z.infer; -/** Result for droid.clear_mcp_auth response. */ export const ClearMcpAuthResultSchema = SuccessResultSchema; export type ClearMcpAuthResult = z.infer; -/** Result for droid.submit_mcp_auth_code response. */ export const SubmitMcpAuthCodeResultSchema = SuccessResultSchema; export type SubmitMcpAuthCodeResult = z.infer< typeof SubmitMcpAuthCodeResultSchema >; -/** Result for droid.add_mcp_server response. */ export const AddMcpServerResultSchema = SuccessResultSchema; export type AddMcpServerResult = z.infer; -/** Result for droid.remove_mcp_server response. */ export const RemoveMcpServerResultSchema = SuccessResultSchema; export type RemoveMcpServerResult = z.infer; -/** Result for droid.list_mcp_registry response. */ export const ListMcpRegistryResultSchema = z .object({ servers: z.array(McpRegistryServerSchema) }) .passthrough(); export type ListMcpRegistryResult = z.infer; -/** Result for droid.list_mcp_tools response. */ export const ListMcpToolsResultSchema = z .object({ tools: z.array(McpToolInfoSchema) }) .passthrough(); export type ListMcpToolsResult = z.infer; -/** Result for droid.list_tools response. */ export const ListToolsResultSchema = z .object({ tools: z.array(ExecToolInfoSchema) }) .passthrough(); export type ListToolsResult = z.infer; -/** Result for droid.list_mcp_servers response. */ export const ListMcpServersResultSchema = z .object({ servers: z.array(McpServerStatusInfoSchema), @@ -1014,26 +950,22 @@ export const ListMcpServersResultSchema = z export type ListMcpServersResult = z.infer; -/** Result for droid.toggle_mcp_tool response. */ export const ToggleMcpToolResultSchema = SuccessResultSchema; export type ToggleMcpToolResult = z.infer; -/** Result for droid.list_skills response. */ export const ListSkillsResultSchema = z .object({ skills: z.array(SkillInfoSchema) }) .passthrough(); export type ListSkillsResult = z.infer; -/** Result for droid.submit_bug_report response. */ export const SubmitBugReportResultSchema = z .object({ bugReportId: z.string() }) .passthrough(); export type SubmitBugReportResult = z.infer; -/** Result for droid.get_rewind_info response. */ export const GetRewindInfoResultSchema = z .object({ availableFiles: z.array(RewindFileSnapshotSchema), @@ -1044,7 +976,6 @@ export const GetRewindInfoResultSchema = z export type GetRewindInfoResult = z.infer; -/** Result for droid.execute_rewind response. */ export const ExecuteRewindResultSchema = z .object({ newSessionId: z.string(), @@ -1057,7 +988,6 @@ export const ExecuteRewindResultSchema = z export type ExecuteRewindResult = z.infer; -/** Result for droid.compact_session response. */ export const CompactSessionResultSchema = z .object({ newSessionId: z.string(), @@ -1067,7 +997,6 @@ export const CompactSessionResultSchema = z export type CompactSessionResult = z.infer; -/** Result for droid.fork_session response. */ export const ForkSessionResultSchema = z .object({ newSessionId: z.string(), @@ -1076,23 +1005,17 @@ export const ForkSessionResultSchema = z export type ForkSessionResult = z.infer; -/** Result for droid.get_context_stats response. */ export const GetContextStatsResultSchema = ContextStatsSchema; export type GetContextStatsResult = z.infer; -/** Result for droid.rename_session response. */ export const RenameSessionResultSchema = SuccessResultSchema; export type RenameSessionResult = z.infer; -export const InitializeSessionResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: InitializeSessionResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const InitializeSessionResponseSchema = createResponseSchema( + InitializeSessionResultSchema +); export type InitializeSessionResponse = z.infer< typeof InitializeSessionResponseSchema >; @@ -1104,10 +1027,9 @@ export type LoadSessionResponse = }) | JsonRpcResponseFailure; -const _LoadSessionResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: LoadSessionResultSchema }), - JsonRpcResponseFailureSchema, -]); +const _LoadSessionResponseSchema = createResponseSchema( + LoadSessionResultSchema +); export const LoadSessionResponseSchema: z.ZodType< LoadSessionResponse, @@ -1120,219 +1042,153 @@ export const LoadSessionResponseSchema: z.ZodType< unknown >; -export const AddUserMessageResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: AddUserMessageResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const AddUserMessageResponseSchema = createResponseSchema( + AddUserMessageResultSchema +); export type AddUserMessageResponse = z.infer< typeof AddUserMessageResponseSchema >; -export const InterruptSessionResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: InterruptSessionResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const InterruptSessionResponseSchema = createResponseSchema( + InterruptSessionResultSchema +); export type InterruptSessionResponse = z.infer< typeof InterruptSessionResponseSchema >; -export const CloseSessionResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: CloseSessionResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const CloseSessionResponseSchema = createResponseSchema( + CloseSessionResultSchema +); export type CloseSessionResponse = z.infer; -export const KillWorkerSessionResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: KillWorkerSessionResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const KillWorkerSessionResponseSchema = createResponseSchema( + KillWorkerSessionResultSchema +); export type KillWorkerSessionResponse = z.infer< typeof KillWorkerSessionResponseSchema >; -export const UpdateSessionSettingsResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: UpdateSessionSettingsResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const UpdateSessionSettingsResponseSchema = createResponseSchema( + UpdateSessionSettingsResultSchema +); export type UpdateSessionSettingsResponse = z.infer< typeof UpdateSessionSettingsResponseSchema >; -export const ToggleMcpServerResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: ToggleMcpServerResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const ToggleMcpServerResponseSchema = createResponseSchema( + ToggleMcpServerResultSchema +); export type ToggleMcpServerResponse = z.infer< typeof ToggleMcpServerResponseSchema >; -export const AuthenticateMcpServerResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: AuthenticateMcpServerResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const AuthenticateMcpServerResponseSchema = createResponseSchema( + AuthenticateMcpServerResultSchema +); export type AuthenticateMcpServerResponse = z.infer< typeof AuthenticateMcpServerResponseSchema >; -export const CancelMcpAuthResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: CancelMcpAuthResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const CancelMcpAuthResponseSchema = createResponseSchema( + CancelMcpAuthResultSchema +); export type CancelMcpAuthResponse = z.infer; -export const ClearMcpAuthResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: ClearMcpAuthResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const ClearMcpAuthResponseSchema = createResponseSchema( + ClearMcpAuthResultSchema +); export type ClearMcpAuthResponse = z.infer; -export const SubmitMcpAuthCodeResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: SubmitMcpAuthCodeResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const SubmitMcpAuthCodeResponseSchema = createResponseSchema( + SubmitMcpAuthCodeResultSchema +); export type SubmitMcpAuthCodeResponse = z.infer< typeof SubmitMcpAuthCodeResponseSchema >; -export const AddMcpServerResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: AddMcpServerResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const AddMcpServerResponseSchema = createResponseSchema( + AddMcpServerResultSchema +); export type AddMcpServerResponse = z.infer; -export const RemoveMcpServerResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: RemoveMcpServerResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const RemoveMcpServerResponseSchema = createResponseSchema( + RemoveMcpServerResultSchema +); export type RemoveMcpServerResponse = z.infer< typeof RemoveMcpServerResponseSchema >; -export const ListMcpRegistryResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: ListMcpRegistryResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const ListMcpRegistryResponseSchema = createResponseSchema( + ListMcpRegistryResultSchema +); export type ListMcpRegistryResponse = z.infer< typeof ListMcpRegistryResponseSchema >; -export const ListMcpToolsResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: ListMcpToolsResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const ListMcpToolsResponseSchema = createResponseSchema( + ListMcpToolsResultSchema +); export type ListMcpToolsResponse = z.infer; -export const ListToolsResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: ListToolsResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const ListToolsResponseSchema = createResponseSchema( + ListToolsResultSchema +); export type ListToolsResponse = z.infer; -export const ListMcpServersResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: ListMcpServersResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const ListMcpServersResponseSchema = createResponseSchema( + ListMcpServersResultSchema +); export type ListMcpServersResponse = z.infer< typeof ListMcpServersResponseSchema >; -export const ToggleMcpToolResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: ToggleMcpToolResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const ToggleMcpToolResponseSchema = createResponseSchema( + ToggleMcpToolResultSchema +); export type ToggleMcpToolResponse = z.infer; -export const ListSkillsResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: ListSkillsResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const ListSkillsResponseSchema = createResponseSchema( + ListSkillsResultSchema +); export type ListSkillsResponse = z.infer; -export const SubmitBugReportResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: SubmitBugReportResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const SubmitBugReportResponseSchema = createResponseSchema( + SubmitBugReportResultSchema +); export type SubmitBugReportResponse = z.infer< typeof SubmitBugReportResponseSchema >; -export const GetRewindInfoResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: GetRewindInfoResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const GetRewindInfoResponseSchema = createResponseSchema( + GetRewindInfoResultSchema +); export type GetRewindInfoResponse = z.infer; -export const ExecuteRewindResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: ExecuteRewindResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const ExecuteRewindResponseSchema = createResponseSchema( + ExecuteRewindResultSchema +); export type ExecuteRewindResponse = z.infer; -export const CompactSessionResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: CompactSessionResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const CompactSessionResponseSchema = createResponseSchema( + CompactSessionResultSchema +); export type CompactSessionResponse = z.infer< typeof CompactSessionResponseSchema >; -export const ForkSessionResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: ForkSessionResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const ForkSessionResponseSchema = createResponseSchema( + ForkSessionResultSchema +); export type ForkSessionResponse = z.infer; -export const GetContextStatsResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: GetContextStatsResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const GetContextStatsResponseSchema = createResponseSchema( + GetContextStatsResultSchema +); export type GetContextStatsResponse = z.infer< typeof GetContextStatsResponseSchema >; -export const RenameSessionResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: RenameSessionResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const RenameSessionResponseSchema = createResponseSchema( + RenameSessionResultSchema +); export type RenameSessionResponse = z.infer; diff --git a/src/schemas/constants.ts b/src/schemas/constants.ts index e3bfbde..d8c9189 100644 --- a/src/schemas/constants.ts +++ b/src/schemas/constants.ts @@ -2,8 +2,9 @@ export const JSONRPC_VERSION = '2.0' as const; /** - * Legacy Factory API version for backward compatibility. - * @deprecated Use FACTORY_PROTOCOL_VERSION for runtime compatibility instead. + * Frozen Factory API version embedded in every JSON-RPC envelope. + * This value is required by the wire protocol and must not be changed. + * For version negotiation, use FACTORY_PROTOCOL_VERSION instead. */ export const LEGACY_FACTORY_API_VERSION = '1.0.0' as const; diff --git a/src/schemas/enums.ts b/src/schemas/enums.ts index c2dd815..7907de8 100644 --- a/src/schemas/enums.ts +++ b/src/schemas/enums.ts @@ -1,12 +1,3 @@ -/** - * All protocol enums for the Factory Droid SDK. - * - * - packages/common/src/droid/enums.ts - * - packages/common/src/shared/enums.ts - * - packages/common/src/llm/enums.ts - * - packages/common/src/settings/enums.ts - */ - /** Droid server methods (client → server communication). */ export enum DroidServerMethod { INITIALIZE_SESSION = 'droid.initialize_session', @@ -45,7 +36,6 @@ export enum DroidClientMethod { ASK_USER = 'droid.ask_user', } -/** Session notification types. */ export enum SessionNotificationType { TOOL_RESULT = 'tool_result', TOOL_PROGRESS_UPDATE = 'tool_progress_update', @@ -104,7 +94,6 @@ export enum ToolConfirmationType { McpTool = 'mcp_tool', } -/** Droid working state (represents what the agent is currently doing). */ export enum DroidWorkingState { Idle = 'idle', StreamingAssistantMessage = 'streaming_assistant_message', @@ -113,13 +102,11 @@ export enum DroidWorkingState { CompactingConversation = 'compacting_conversation', } -/** Accuracy of reported context stats. */ export enum ContextStatsAccuracy { Exact = 'exact', Estimated = 'estimated', } -/** Error types for error notifications. */ export enum DroidErrorType { CONNECTION_ERROR = 'ConnectionError', PROTOCOL_ERROR = 'ProtocolError', @@ -130,7 +117,6 @@ export enum DroidErrorType { ERROR = 'Error', } -/** MCP server connection status. */ export enum McpServerStatus { Connecting = 'connecting', Connected = 'connected', @@ -139,14 +125,12 @@ export enum McpServerStatus { Disabled = 'disabled', } -/** MCP server transport type. */ export enum McpServerType { Stdio = 'stdio', Http = 'http', Sse = 'sse', } -/** Overall MCP initialization status. */ export enum McpStatus { NotInitialized = 'not-initialized', Initializing = 'initializing', @@ -155,7 +139,6 @@ export enum McpStatus { Failed = 'failed', } -/** MCP authentication outcome. */ export enum McpAuthOutcome { Success = 'success', Cancelled = 'cancelled', @@ -260,7 +243,6 @@ export enum AutonomyMode { AutoHigh = 'auto-high', } -/** Reasoning effort levels for LLMs. */ export enum ReasoningEffort { None = 'none', Dynamic = 'dynamic', @@ -273,7 +255,6 @@ export enum ReasoningEffort { Max = 'max', } -/** Model provider identifiers. */ export enum ModelProvider { ANTHROPIC = 'anthropic', OPENAI = 'openai', @@ -296,7 +277,6 @@ export enum JsonRpcErrorCode { SESSION_DISCONNECTED = -32005, } -/** JSON-RPC message type discriminator. */ export enum JsonRpcMessageType { Request = 'request', Response = 'response', @@ -317,7 +297,6 @@ export enum SettingsLevel { BuiltIn = 'builtin', } -/** Skill file location type. */ export enum SkillLocation { Project = 'project', Personal = 'personal', diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 5282b99..b9ae4b6 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -47,6 +47,7 @@ export { BaseRequestSchema, BaseResponseFailureSchema, BaseResponseSuccessSchema, + createResponseSchema, EmptyResultSchema, JsonArraySchema, JsonRpcEnvelopeSchema, diff --git a/src/schemas/mcp.ts b/src/schemas/mcp.ts index fa8ec03..6296935 100644 --- a/src/schemas/mcp.ts +++ b/src/schemas/mcp.ts @@ -8,7 +8,6 @@ import { } from './enums.js'; import { JsonObjectSchema } from './shared.js'; -/** Stdio MCP server configuration fields. */ export const McpStdioServerConfigFieldsSchema = z.object({ command: z.string().optional(), args: z.array(z.string()).optional(), @@ -18,7 +17,6 @@ export type McpStdioServerConfigFields = z.infer< typeof McpStdioServerConfigFieldsSchema >; -/** HTTP MCP server configuration fields. */ export const McpHttpServerConfigFieldsSchema = z.object({ url: z.string().optional(), }); @@ -27,7 +25,6 @@ export type McpHttpServerConfigFields = z.infer< typeof McpHttpServerConfigFieldsSchema >; -/** SSE MCP server configuration fields. */ export const McpSseServerConfigFieldsSchema = z.object({ url: z.string(), headers: z.record(z.string()).optional(), @@ -37,7 +34,6 @@ export type McpSseServerConfigFields = z.infer< typeof McpSseServerConfigFieldsSchema >; -/** MCP server status information. */ export const McpServerStatusInfoSchema = z .object({ name: z.string(), @@ -53,7 +49,6 @@ export const McpServerStatusInfoSchema = z export type McpServerStatusInfo = z.infer; -/** MCP status summary. */ export const McpStatusSummarySchema = z .object({ total: z.number(), @@ -66,7 +61,6 @@ export const McpStatusSummarySchema = z export type McpStatusSummary = z.infer; -/** MCP registry server entity. */ export const McpRegistryServerSchema = z .object({ name: z.string(), @@ -96,7 +90,6 @@ export const McpToolInputSchemaSchema = z export type McpToolInputSchema = z.infer; -/** MCP tool information entity. */ export const McpToolInfoSchema = z .object({ serverName: z.string(), @@ -110,7 +103,6 @@ export const McpToolInfoSchema = z export type McpToolInfo = z.infer; -/** Selectable list item for tool confirmation prompts. */ export const ToolConfirmationListItemSchema = z .object({ label: z.string(), diff --git a/src/schemas/server.ts b/src/schemas/server.ts index e4afabe..92febdb 100644 --- a/src/schemas/server.ts +++ b/src/schemas/server.ts @@ -27,19 +27,14 @@ import { } from './messages.js'; import { MissionFeatureSchema, ProgressLogEntrySchema } from './mission.js'; import { + createResponseSchema, JsonObjectSchema, JsonRpcNotificationSchema, JsonRpcRequestSchema, - JsonRpcResponseFailureSchema, - JsonRpcResponseSuccessSchema, JsonValueSchema, ToolSelectionOverridesSchema, } from './shared.js'; -/** - * Tool use block — re-exports ToolUseBlockSchema from messages.ts - * since the shape is identical (type, id, input, name, thoughtSignature). - */ export const ToolUseBlockSchema = MessageToolUseBlockSchema; export type ToolUseBlock = z.infer; @@ -50,7 +45,6 @@ export const ToolUseSchema = ToolUseBlockSchema; /** @deprecated Use ToolUseBlock for the message block shape. */ export type ToolUse = ToolUseBlock; -/** Error detail object within an ErrorNotification. */ export const ErrorDetailSchema = z .object({ name: z.string(), @@ -60,7 +54,6 @@ export const ErrorDetailSchema = z export type ErrorDetail = z.infer; -/** Streaming update from subagent tool calls. */ export const ToolProgressUpdateSchema = z .object({ type: z.enum(['tool_call', 'tool_result', 'error', 'status', 'message']), @@ -78,7 +71,6 @@ export const ToolProgressUpdateSchema = z export type ToolProgressUpdate = z.infer; -/** Settings payload within SettingsUpdatedNotification. */ export const SettingsUpdatedPayloadSchema = z .object({ autonomyMode: z.nativeEnum(AutonomyMode).optional(), @@ -99,7 +91,6 @@ export type SettingsUpdatedPayload = z.infer< typeof SettingsUpdatedPayloadSchema >; -/** Tool result notification. */ export const ToolResultNotificationSchema = z .object({ type: z.literal(SessionNotificationType.TOOL_RESULT), @@ -115,7 +106,6 @@ export type ToolResultNotification = z.infer< typeof ToolResultNotificationSchema >; -/** Tool progress update notification. */ export const ToolProgressUpdateNotificationSchema = z .object({ type: z.literal(SessionNotificationType.TOOL_PROGRESS_UPDATE), @@ -129,7 +119,6 @@ export type ToolProgressUpdateNotification = z.infer< typeof ToolProgressUpdateNotificationSchema >; -/** Create message notification. */ export const CreateMessageNotificationSchema = z .object({ type: z.literal(SessionNotificationType.CREATE_MESSAGE), @@ -143,7 +132,6 @@ export type CreateMessageNotification = z.infer< typeof CreateMessageNotificationSchema >; -/** Error notification. */ export const ErrorNotificationSchema = z .object({ type: z.literal(SessionNotificationType.ERROR), @@ -156,7 +144,6 @@ export const ErrorNotificationSchema = z export type ErrorNotification = z.infer; -/** Droid working state changed notification. */ export const DroidWorkingStateChangedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.DROID_WORKING_STATE_CHANGED), @@ -168,7 +155,6 @@ export type DroidWorkingStateChangedNotification = z.infer< typeof DroidWorkingStateChangedNotificationSchema >; -/** Permission resolved notification. */ export const PermissionResolvedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.PERMISSION_RESOLVED), @@ -182,7 +168,6 @@ export type PermissionResolvedNotification = z.infer< typeof PermissionResolvedNotificationSchema >; -/** Settings updated notification. */ export const SettingsUpdatedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.SETTINGS_UPDATED), @@ -194,7 +179,6 @@ export type SettingsUpdatedNotification = z.infer< typeof SettingsUpdatedNotificationSchema >; -/** Session title updated notification. */ export const SessionTitleUpdatedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.SESSION_TITLE_UPDATED), @@ -206,7 +190,6 @@ export type SessionTitleUpdatedNotification = z.infer< typeof SessionTitleUpdatedNotificationSchema >; -/** MCP status changed notification. */ export const McpStatusChangedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.MCP_STATUS_CHANGED), @@ -246,7 +229,6 @@ export type AssistantTextCompleteNotification = z.infer< typeof AssistantTextCompleteNotificationSchema >; -/** Thinking text delta notification (streaming thinking token). */ export const ThinkingTextDeltaNotificationSchema = z .object({ type: z.literal(SessionNotificationType.THINKING_TEXT_DELTA), @@ -260,7 +242,6 @@ export type ThinkingTextDeltaNotification = z.infer< typeof ThinkingTextDeltaNotificationSchema >; -/** Thinking text complete notification (streaming thinking block finished). */ export const ThinkingTextCompleteNotificationSchema = z .object({ type: z.literal(SessionNotificationType.THINKING_TEXT_COMPLETE), @@ -274,7 +255,6 @@ export type ThinkingTextCompleteNotification = z.infer< typeof ThinkingTextCompleteNotificationSchema >; -/** Tool call partial notification. */ export const ToolCallNotificationSchema = z .object({ type: z.literal(SessionNotificationType.TOOL_CALL), @@ -284,7 +264,6 @@ export const ToolCallNotificationSchema = z export type ToolCallNotification = z.infer; -/** Session token usage changed notification. */ export const SessionTokenUsageChangedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.SESSION_TOKEN_USAGE_CHANGED), @@ -297,7 +276,6 @@ export type SessionTokenUsageChangedNotification = z.infer< typeof SessionTokenUsageChangedNotificationSchema >; -/** Mission state changed notification. */ export const MissionStateChangedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.MISSION_STATE_CHANGED), @@ -309,7 +287,6 @@ export type MissionStateChangedNotification = z.infer< typeof MissionStateChangedNotificationSchema >; -/** Mission features changed notification. */ export const MissionFeaturesChangedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.MISSION_FEATURES_CHANGED), @@ -321,7 +298,6 @@ export type MissionFeaturesChangedNotification = z.infer< typeof MissionFeaturesChangedNotificationSchema >; -/** Mission progress entry notification. */ export const MissionProgressEntryNotificationSchema = z .object({ type: z.literal(SessionNotificationType.MISSION_PROGRESS_ENTRY), @@ -333,7 +309,6 @@ export type MissionProgressEntryNotification = z.infer< typeof MissionProgressEntryNotificationSchema >; -/** Mission heartbeat notification. */ export const MissionHeartbeatNotificationSchema = z .object({ type: z.literal(SessionNotificationType.MISSION_HEARTBEAT), @@ -345,7 +320,6 @@ export type MissionHeartbeatNotification = z.infer< typeof MissionHeartbeatNotificationSchema >; -/** Mission worker started notification. */ export const MissionWorkerStartedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.MISSION_WORKER_STARTED), @@ -357,7 +331,6 @@ export type MissionWorkerStartedNotification = z.infer< typeof MissionWorkerStartedNotificationSchema >; -/** Mission worker completed notification. */ export const MissionWorkerCompletedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.MISSION_WORKER_COMPLETED), @@ -370,7 +343,6 @@ export type MissionWorkerCompletedNotification = z.infer< typeof MissionWorkerCompletedNotificationSchema >; -/** MCP authentication required notification. */ export const McpAuthRequiredNotificationSchema = z .object({ type: z.literal(SessionNotificationType.MCP_AUTH_REQUIRED), @@ -385,7 +357,6 @@ export type McpAuthRequiredNotification = z.infer< typeof McpAuthRequiredNotificationSchema >; -/** MCP authentication completed notification. */ export const McpAuthCompletedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.MCP_AUTH_COMPLETED), @@ -399,7 +370,6 @@ export type McpAuthCompletedNotification = z.infer< typeof McpAuthCompletedNotificationSchema >; -/** Hook command metadata included in hook execution notifications. */ export const HookCommandSchema = z .object({ command: z.string(), @@ -409,7 +379,6 @@ export const HookCommandSchema = z export type HookCommand = z.infer; -/** Hook execution result included in hook completion notifications. */ export const HookResultSchema = z .object({ exitCode: z.number(), @@ -422,7 +391,6 @@ export const HookResultSchema = z export type HookResult = z.infer; -/** Hook execution started notification. */ export const HookExecutionStartedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.HOOK_EXECUTION_STARTED), @@ -440,7 +408,6 @@ export type HookExecutionStartedNotification = z.infer< typeof HookExecutionStartedNotificationSchema >; -/** Hook execution completed notification. */ export const HookExecutionCompletedNotificationSchema = z .object({ type: z.literal(SessionNotificationType.HOOK_EXECUTION_COMPLETED), @@ -781,13 +748,9 @@ export type RequestPermissionResult = z.infer< >; /** Response to droid.request_permission. */ -export const RequestPermissionResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ - result: RequestPermissionResultSchema, - }), - JsonRpcResponseFailureSchema, -]); - +export const RequestPermissionResponseSchema = createResponseSchema( + RequestPermissionResultSchema +); export type RequestPermissionResponse = z.infer< typeof RequestPermissionResponseSchema >; @@ -834,11 +797,7 @@ export const AskUserResultSchema = z export type AskUserResult = z.infer; /** Response to droid.ask_user. */ -export const AskUserResponseSchema = z.union([ - JsonRpcResponseSuccessSchema.extend({ result: AskUserResultSchema }), - JsonRpcResponseFailureSchema, -]); - +export const AskUserResponseSchema = createResponseSchema(AskUserResultSchema); export type AskUserResponse = z.infer; /** Union over all server → client methods. */ diff --git a/src/schemas/shared.ts b/src/schemas/shared.ts index 5cc8b29..57e1ac3 100644 --- a/src/schemas/shared.ts +++ b/src/schemas/shared.ts @@ -172,3 +172,11 @@ export const JsonRpcMessageSchema = z.discriminatedUnion('type', [ ]); export type JsonRpcMessage = z.infer; + +/** Creates a JSON-RPC response schema (success | failure) for a given result schema. */ +export function createResponseSchema(resultSchema: T) { + return z.union([ + JsonRpcResponseSuccessSchema.extend({ result: resultSchema }), + JsonRpcResponseFailureSchema, + ]); +} diff --git a/src/session.ts b/src/session.ts index 8295522..5bce17f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -48,7 +48,6 @@ import type { } from './schemas/client.js'; import { DroidInteractionMode } from './schemas/enums.js'; import type { Base64ImageSource, DocumentSource } from './schemas/messages.js'; -import { DroidMessageType } from './stream.js'; import type { DroidResultMessage, DroidStreamEvent, @@ -100,38 +99,6 @@ function throwIfAborted(signal: AbortSignal | undefined): void { } } -export function aggregateMessages( - sessionId: string, - messages: DroidStreamEvent[], - startedAt: number, - _options?: MessageOptions -): DroidResult { - const result = messages.find( - (message): message is DroidResultMessage => - message.type === DroidMessageType.Result - ); - if (result) { - return result; - } - - return { - type: DroidMessageType.Result, - subtype: 'error_during_execution', - sessionId, - durationMs: Date.now() - startedAt, - isError: true, - numTurns: 0, - result: '', - tokenUsage: null, - errors: ['Stream completed without a result message'], - messages, - text: '', - turnCount: 0, - success: false, - error: null, - }; -} - /** Create instances via {@link createSession} or {@link resumeSession}. */ export class DroidSession { private _client: DroidClient; diff --git a/src/stream.ts b/src/stream.ts index d1af7d7..60d31bc 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -773,17 +773,6 @@ export class StreamStateTracker { } } -export function isPartialMessage(message: InternalDroidMessage): boolean { - return ( - message.type === DroidMessageType.AssistantTextDelta || - message.type === DroidMessageType.AssistantTextComplete || - message.type === DroidMessageType.ThinkingTextDelta || - message.type === DroidMessageType.ThinkingTextComplete || - message.type === DroidMessageType.ToolCallDelta || - message.type === DroidMessageType.ToolProgress - ); -} - export function isInternalMessage(message: InternalDroidMessage): boolean { return ( message.type === 'structured_output' || diff --git a/tests/code-quality.test.ts b/tests/code-quality.test.ts new file mode 100644 index 0000000..d0b4b61 --- /dev/null +++ b/tests/code-quality.test.ts @@ -0,0 +1,199 @@ +import { describe, expect, it, vi } from 'vitest'; +import { z } from 'zod'; + +import { dispatchNotification } from '../src/protocol.js'; +import type { NotificationListener } from '../src/protocol.js'; +import { SessionNotificationType } from '../src/schemas/enums.js'; +import { + createResponseSchema, + JSONRPC_VERSION, + LEGACY_FACTORY_API_VERSION, + FACTORY_PROTOCOL_VERSION, + InitializeSessionResponseSchema, + CloseSessionResponseSchema, + ListSkillsResponseSchema, +} from '../src/schemas/index.js'; +import { makeSessionNotification } from './helpers.js'; + +const envelope = { + jsonrpc: JSONRPC_VERSION, + factoryApiVersion: LEGACY_FACTORY_API_VERSION, + factoryProtocolVersion: FACTORY_PROTOCOL_VERSION, + type: 'response' as const, +}; + +describe('createResponseSchema', () => { + const TestResultSchema = z.object({ value: z.string() }).strict(); + const TestResponseSchema = createResponseSchema(TestResultSchema); + + it('accepts a valid success response', () => { + const input = { + ...envelope, + id: 'req-1', + result: { value: 'hello' }, + }; + const parsed = TestResponseSchema.parse(input); + expect(parsed).toHaveProperty('result'); + expect((parsed as { result: { value: string } }).result.value).toBe( + 'hello' + ); + }); + + it('accepts a valid failure response', () => { + const input = { + ...envelope, + id: null, + error: { code: -32600, message: 'Invalid Request' }, + }; + const parsed = TestResponseSchema.parse(input); + expect(parsed).toHaveProperty('error'); + }); + + it('rejects a response with wrong result shape', () => { + const input = { + ...envelope, + id: 'req-2', + result: { wrong: 123 }, + }; + const result = TestResponseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + it('rejects a response missing both result and error', () => { + const input = { + ...envelope, + id: 'req-3', + }; + const result = TestResponseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + it('produces identical parse results to hand-written schemas', () => { + const successInput = { + ...envelope, + id: 'req-4', + result: { + sessionId: 'sess-1', + session: {}, + settings: { modelId: 'test', reasoningEffort: 'medium' }, + }, + }; + + const factoryResult = + InitializeSessionResponseSchema.safeParse(successInput); + expect(factoryResult.success).toBe(true); + + const failureInput = { + ...envelope, + id: null, + error: { code: -32603, message: 'Internal error' }, + }; + const factoryFailResult = + InitializeSessionResponseSchema.safeParse(failureInput); + expect(factoryFailResult.success).toBe(true); + }); + + it('factory-generated schemas reject malformed responses', () => { + const malformed = { + ...envelope, + id: 'req-5', + result: 'not-an-object', + }; + + expect(CloseSessionResponseSchema.safeParse(malformed).success).toBe(false); + expect(ListSkillsResponseSchema.safeParse(malformed).success).toBe(false); + }); +}); + +describe('dispatchNotification', () => { + it('dispatches to all listeners when no filter is set', () => { + const callback1 = vi.fn(); + const callback2 = vi.fn(); + const listeners: NotificationListener[] = [ + { callback: callback1 }, + { callback: callback2 }, + ]; + + const notification = makeSessionNotification( + SessionNotificationType.ERROR, + { message: 'test', errorType: 'Error', timestamp: '2024-01-01' } + ); + + dispatchNotification(notification, listeners); + + expect(callback1).toHaveBeenCalledOnce(); + expect(callback2).toHaveBeenCalledOnce(); + expect(callback1).toHaveBeenCalledWith(notification); + }); + + it('filters listeners by notification type', () => { + const errorCallback = vi.fn(); + const stateCallback = vi.fn(); + const listeners: NotificationListener[] = [ + { + callback: errorCallback, + filter: { type: SessionNotificationType.ERROR }, + }, + { + callback: stateCallback, + filter: { type: SessionNotificationType.DROID_WORKING_STATE_CHANGED }, + }, + ]; + + const notification = makeSessionNotification( + SessionNotificationType.ERROR, + { message: 'test', errorType: 'Error', timestamp: '2024-01-01' } + ); + + dispatchNotification(notification, listeners); + + expect(errorCallback).toHaveBeenCalledOnce(); + expect(stateCallback).not.toHaveBeenCalled(); + }); + + it('does not crash when a listener throws', () => { + const throwingCallback = vi.fn(() => { + throw new Error('listener error'); + }); + const normalCallback = vi.fn(); + const listeners: NotificationListener[] = [ + { callback: throwingCallback }, + { callback: normalCallback }, + ]; + + const notification = makeSessionNotification( + SessionNotificationType.ERROR, + { message: 'test', errorType: 'Error', timestamp: '2024-01-01' } + ); + + expect(() => dispatchNotification(notification, listeners)).not.toThrow(); + expect(throwingCallback).toHaveBeenCalledOnce(); + expect(normalCallback).toHaveBeenCalledOnce(); + }); + + it('handles notifications without parseable params', () => { + const callback = vi.fn(); + const listeners: NotificationListener[] = [{ callback }]; + const notification = { method: 'some.method', params: { bad: 'data' } }; + + dispatchNotification(notification, listeners); + + expect(callback).toHaveBeenCalledOnce(); + }); + + it('skips filtered listeners for unparseable notifications', () => { + const filteredCallback = vi.fn(); + const unfilteredCallback = vi.fn(); + const listeners: NotificationListener[] = [ + { callback: filteredCallback, filter: { type: 'some_type' } }, + { callback: unfilteredCallback }, + ]; + + const notification = { method: 'some.method', params: {} }; + + dispatchNotification(notification, listeners); + + expect(filteredCallback).not.toHaveBeenCalled(); + expect(unfilteredCallback).toHaveBeenCalledOnce(); + }); +});