From fa7fed0d32e95e4437195b47d6ea886f42b2e284 Mon Sep 17 00:00:00 2001 From: User Date: Fri, 29 May 2026 16:57:20 -0700 Subject: [PATCH] feat: add protocol TIER-3 schemas (cli, client, mcp, custom-models, mission-decomposition) Verbatim mirrors of the private factory-mono-alpha CLI/client wire protocol, extending the public source-of-truth protocol surface. Not yet wired into the public package API. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/protocol/cli.ts | 563 +++++++++++++ src/protocol/client.ts | 1104 +++++++++++++++++++++++++ src/protocol/custom-models.ts | 85 ++ src/protocol/enums.ts | 47 ++ src/protocol/index.ts | 5 + src/protocol/mcp.ts | 77 ++ src/protocol/mission-decomposition.ts | 346 ++++++++ tests/protocol-dir.test.ts | 73 ++ 8 files changed, 2300 insertions(+) create mode 100644 src/protocol/cli.ts create mode 100644 src/protocol/client.ts create mode 100644 src/protocol/custom-models.ts create mode 100644 src/protocol/mcp.ts create mode 100644 src/protocol/mission-decomposition.ts diff --git a/src/protocol/cli.ts b/src/protocol/cli.ts new file mode 100644 index 0000000..be643bc --- /dev/null +++ b/src/protocol/cli.ts @@ -0,0 +1,563 @@ +// Source-of-truth mirror of factory-mono-alpha CLI protocol schemas. +// Verbatim copy of packages/common/src/droid/schemas/cli.ts. + +import { z } from 'zod'; + +import { SandboxModeSchema } from './custom-models.js'; +import { + AutonomyLevel, + AutonomyMode, + DroidClientMethod, + DroidErrorType, + DroidWorkingState, + McpAuthOutcome, + MissionState, + ReasoningEffort, + SandboxOperationType, + SandboxViolationReason, + SandboxViolationType, + SessionNotificationType, + ToolConfirmationOutcome, + ToolConfirmationType, +} from './enums.js'; +import { + DroidInteractionModeSchema, + JsonRpcBaseNotificationSchema, + JsonRpcBaseRequestSchema, + JsonRpcBaseResponseFailureSchema, + JsonRpcBaseResponseSuccessSchema, + JsonRpcEnvelopeSchema, +} from './json-rpc.js'; +import { LoopStateSchema } from './loop.js'; +import { McpServerStatusInfoSchema, McpStatusSummarySchema } from './mcp.js'; +import { + FactoryDroidMessageSchema, + ToolResultSchema, + ToolUseSchema, +} from './messages.js'; +import { + MissionFeatureSchema, + ProgressLogEntrySchema, +} from './mission-decomposition.js'; +import { MissionModelSettingsSchema } from './model-settings.js'; +import { ToolConfirmationListItemSchema } from './selectable-list-item.js'; +import { TokenUsageSchema } from './session.js'; + +const ToolResultNotificationSchema = ToolResultSchema.extend({ + type: z.literal(SessionNotificationType.TOOL_RESULT), + messageId: z.string(), +}); + +// Schema for streaming updates from subagent tool calls +export const ToolProgressUpdateSchema = z.object({ + type: z.enum(['tool_call', 'tool_result', 'error', 'status', 'message']), + toolName: z.string().optional(), + status: z.string().optional(), + details: z.string().optional(), + text: z.string().optional(), + error: z.string().optional(), + timestamp: z.number().optional(), + parameters: z.record(z.unknown()).optional(), + valueSnippet: z.string().optional(), + terminalId: z.string().optional(), + fullOutput: z.string().optional(), + /** The session ID of the spawned subagent, parsed from a system/init debug event */ + subagentSessionId: z.string().optional(), +}); + +const ToolProgressUpdateNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.TOOL_PROGRESS_UPDATE), + toolUseId: z.string(), + toolName: z.string(), + update: ToolProgressUpdateSchema, +}); + +const CreateMessageNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.CREATE_MESSAGE), + message: FactoryDroidMessageSchema, + parentId: z.string().optional(), + requestId: z.string().optional(), +}); + +const ErrorNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.ERROR), + message: z.string(), + errorType: z.nativeEnum(DroidErrorType), + timestamp: z.string(), + error: z + .object({ + name: z.string(), + message: z.string(), + }) + .optional(), +}); + +const DroidWorkingStateChangedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.DROID_WORKING_STATE_CHANGED), + newState: z.nativeEnum(DroidWorkingState), +}); + +export const SessionCompactedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.SESSION_COMPACTED), + summaryId: z.string(), + removedCount: z.number().nonnegative(), + visibleBoundaryMessageId: z.string().nullable(), +}); + +export const LoopStateChangedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.LOOP_STATE_CHANGED), + loopState: LoopStateSchema, +}); + +const PermissionResolvedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.PERMISSION_RESOLVED), + requestId: z.string(), + toolUseIds: z.array(z.string()), // Array to match batched permission requests + selectedOption: z.nativeEnum(ToolConfirmationOutcome), +}); + +const SettingsUpdatedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.SETTINGS_UPDATED), + requestId: z.string().optional(), + settings: z.object({ + autonomyMode: z + .nativeEnum(AutonomyMode) + .optional() + .describe('Deprecated: use interactionMode + autonomyLevel instead.'), + interactionMode: DroidInteractionModeSchema.optional().catch(undefined), + autonomyLevel: z.nativeEnum(AutonomyLevel).optional().catch(undefined), + modelId: z.string().optional(), + reasoningEffort: z.nativeEnum(ReasoningEffort).optional(), + specModeModelId: z.string().optional(), + specModeReasoningEffort: z.nativeEnum(ReasoningEffort).optional(), + enabledToolIds: z.array(z.string()).optional(), + disabledToolIds: z.array(z.string()).optional(), + missionSettings: MissionModelSettingsSchema.optional(), + compactionThresholdCheckEnabled: z.boolean().optional(), + }), +}); + +const SessionTitleUpdatedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.SESSION_TITLE_UPDATED), + requestId: z.string().optional(), + title: z.string(), +}); + +const McpStatusChangedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.MCP_STATUS_CHANGED), + servers: z.array(McpServerStatusInfoSchema), + summary: McpStatusSummarySchema, +}); + +const AssistantTextDeltaNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.ASSISTANT_TEXT_DELTA), + messageId: z.string(), + blockIndex: z.number(), + textDelta: z.string(), +}); + +const AssistantTextCompleteNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.ASSISTANT_TEXT_COMPLETE), + messageId: z.string(), + blockIndex: z.number(), +}); + +const StructuredOutputNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.STRUCTURED_OUTPUT), + messageId: z.string(), + structuredOutput: z.record(z.unknown()).nullable(), +}); + +const ThinkingTextDeltaNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.THINKING_TEXT_DELTA), + messageId: z.string(), + blockIndex: z.number(), + textDelta: z.string(), +}); + +const ThinkingTextCompleteNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.THINKING_TEXT_COMPLETE), + messageId: z.string(), + blockIndex: z.number(), + durationMs: z.number().nonnegative().optional(), +}); + +export const SessionTokenUsageChangedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.SESSION_TOKEN_USAGE_CHANGED), + sessionId: z.string(), + tokenUsage: TokenUsageSchema, + inclusiveTokenUsage: TokenUsageSchema.optional(), + // Latest provider-reported usage used by the compaction meter. + lastCallTokenUsage: TokenUsageSchema.pick({ + inputTokens: true, + cacheReadTokens: true, + }) + .extend({ + outputTokens: TokenUsageSchema.shape.outputTokens.optional(), + }) + .optional(), +}); + +// Mission notification schemas +export const MissionStateChangedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.MISSION_STATE_CHANGED), + state: z.nativeEnum(MissionState), + updatedAt: z.string().optional(), +}); + +export const MissionFeaturesChangedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.MISSION_FEATURES_CHANGED), + features: z.array(MissionFeatureSchema), +}); + +export const MissionProgressEntryNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.MISSION_PROGRESS_ENTRY), + progressLog: z.array(ProgressLogEntrySchema), +}); + +export const MissionHeartbeatNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.MISSION_HEARTBEAT), + timestamp: z.string(), +}); + +export const MissionWorkerStartedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.MISSION_WORKER_STARTED), + workerSessionId: z.string(), +}); + +export const MissionWorkerCompletedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.MISSION_WORKER_COMPLETED), + workerSessionId: z.string(), + exitCode: z.number(), +}); + +const McpAuthRequiredNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.MCP_AUTH_REQUIRED), + serverName: z.string(), + authUrl: z.string(), + message: z.string(), + state: z.string(), +}); + +const McpAuthCompletedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.MCP_AUTH_COMPLETED), + serverName: z.string(), + outcome: z.nativeEnum(McpAuthOutcome), + message: z.string(), +}); + +const HookCommandSchema = z.object({ + command: z.string(), + timeout: z.number().optional(), +}); + +const HookResultSchema = z.object({ + exitCode: z.number(), + stdout: z.string(), + stderr: z.string(), + command: z.string().optional(), + timeout: z.number().optional(), +}); + +export const DroidHookEventSchema = z.enum([ + 'PreToolUse', + 'PostToolUse', + 'Notification', + 'UserPromptSubmit', + 'Stop', + 'SubagentStop', + 'PreCompact', + 'SessionStart', + 'SessionEnd', +]); + +const ToolCallNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.TOOL_CALL), + toolUse: ToolUseSchema, +}); + +const QueuedMessagesDiscardedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.QUEUED_MESSAGES_DISCARDED), + text: z.string(), + requestId: z.string().optional(), +}); + +/** + * Internal keep-alive notification emitted while a long-running tool + * (e.g. Execute/pytest) is actively running without producing new streaming + * output. Consumed by the daemon to refresh the session inactivity timeout; + * never forwarded to external clients. + */ +const ToolExecutionHeartbeatNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.TOOL_EXECUTION_HEARTBEAT), + toolUseId: z.string(), + toolName: z.string(), +}); + +const HookExecutionStartedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.HOOK_EXECUTION_STARTED), + hookId: z.string(), + hookEventName: z.string(), + hookMatcher: z.string().optional(), + hookCommands: z.array(HookCommandSchema), + hookToolCallId: z.string().optional(), + isParallelExecution: z.boolean().optional(), + parallelGroupId: z.string().optional(), +}); + +const HookExecutionCompletedNotificationSchema = z.object({ + type: z.literal(SessionNotificationType.HOOK_EXECUTION_COMPLETED), + hookId: z.string(), + hookEventName: DroidHookEventSchema.optional(), + hookMatcher: z.string().optional(), + hookToolCallId: z.string().optional(), + hookStatus: z.enum(['completed', 'error']), + hookResults: z.array(HookResultSchema).optional(), +}); + +export const SandboxStatusSchema = z.object({ + enabled: z.boolean(), + mode: SandboxModeSchema.optional(), +}); + +export const SessionNotificationSchemaList = [ + ToolResultNotificationSchema, + ToolProgressUpdateNotificationSchema, + CreateMessageNotificationSchema, + ErrorNotificationSchema, + DroidWorkingStateChangedNotificationSchema, + SessionCompactedNotificationSchema, + LoopStateChangedNotificationSchema, + PermissionResolvedNotificationSchema, + SettingsUpdatedNotificationSchema, + SessionTitleUpdatedNotificationSchema, + McpStatusChangedNotificationSchema, + AssistantTextDeltaNotificationSchema, + AssistantTextCompleteNotificationSchema, + StructuredOutputNotificationSchema, + ThinkingTextDeltaNotificationSchema, + ThinkingTextCompleteNotificationSchema, + SessionTokenUsageChangedNotificationSchema, + // Mission notifications + MissionStateChangedNotificationSchema, + MissionFeaturesChangedNotificationSchema, + MissionProgressEntryNotificationSchema, + MissionHeartbeatNotificationSchema, + MissionWorkerStartedNotificationSchema, + MissionWorkerCompletedNotificationSchema, + McpAuthRequiredNotificationSchema, + McpAuthCompletedNotificationSchema, + HookExecutionStartedNotificationSchema, + HookExecutionCompletedNotificationSchema, + ToolCallNotificationSchema, + QueuedMessagesDiscardedNotificationSchema, + ToolExecutionHeartbeatNotificationSchema, +] as const; + +const SessionNotificationParamsSchema = z.object({ + notification: z.discriminatedUnion('type', SessionNotificationSchemaList), +}); + +export const SessionNotificationSchema = JsonRpcBaseNotificationSchema.extend({ + method: z.literal(DroidClientMethod.SESSION_NOTIFICATION), + params: SessionNotificationParamsSchema, +}); + +// Tool Confirmation Details schemas (matching TypeScript types in types.ts) +const EditToolConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.Edit), + filePath: z.string(), + fileName: z.string(), + oldContent: z.string().optional(), + newContent: z.string().optional(), +}); + +const ExecuteToolConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.Execute), + fullCommand: z.string(), + command: z.string(), + extractedCommands: z.array(z.string()).optional(), + impactLevel: z.string().optional(), + riskLevelReason: z.string().optional(), +}); + +const CreateToolConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.Create), + filePath: z.string(), + fileName: z.string(), + content: z.string(), +}); + +const AskUserConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.AskUser), + questionnaire: z.string(), + parsed: z + .object({ + questions: z.array( + z.object({ + index: z.number(), + topic: z.string(), + question: z.string(), + options: z.array(z.string()), + }) + ), + }) + .optional(), + parseError: z + .object({ + message: z.string(), + line: z.number().optional(), + }) + .optional(), +}); + +const ExitSpecModeConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.ExitSpecMode), + plan: z.string(), + title: z.string().optional(), +}); + +const ProposeMissionConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.ProposeMission), + proposal: z.string(), + title: z.string().optional(), +}); + +const StartMissionRunConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.StartMissionRun), + runningMissionCount: z.number(), + runningMissionSessionIds: z.array(z.string()), +}); + +const ApplyPatchToolConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.ApplyPatch), + filePath: z.string(), + fileName: z.string(), + patchContent: z.string(), + oldContent: z.string().optional(), + newContent: z.string().optional(), +}); + +const McpToolConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.McpTool), + toolName: z.string(), + impactLevel: z.string(), + serverName: z.string().optional(), + actualToolName: z.string().optional(), +}); + +const SandboxViolationConfirmationDetailsSchema = z.object({ + type: z.literal(ToolConfirmationType.SandboxViolation), + violatingToolName: z.string(), + target: z.string(), + operationType: z.nativeEnum(SandboxOperationType), + violationType: z.nativeEnum(SandboxViolationType), + reason: z.string(), + violationReason: z.nativeEnum(SandboxViolationReason).optional(), + isOrgDeny: z.boolean(), +}); + +export const ToolConfirmationDetailsSchema = z.discriminatedUnion('type', [ + EditToolConfirmationDetailsSchema, + ExecuteToolConfirmationDetailsSchema, + CreateToolConfirmationDetailsSchema, + AskUserConfirmationDetailsSchema, + ExitSpecModeConfirmationDetailsSchema, + ProposeMissionConfirmationDetailsSchema, + StartMissionRunConfirmationDetailsSchema, + ApplyPatchToolConfirmationDetailsSchema, + McpToolConfirmationDetailsSchema, + SandboxViolationConfirmationDetailsSchema, +]); + +const ToolConfirmationInfoSchema = z.object({ + toolUse: ToolUseSchema, + confirmationType: z.nativeEnum(ToolConfirmationType), + details: ToolConfirmationDetailsSchema, +}); + +export const RequestPermissionRequestParamsSchema = z.object({ + toolUses: z.array(ToolConfirmationInfoSchema), + options: z.array(ToolConfirmationListItemSchema), +}); + +export const RequestPermissionRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidClientMethod.REQUEST_PERMISSION), + params: RequestPermissionRequestParamsSchema, +}); + +// ============================================================ +// Ask User (multi-question user input) +// ============================================================ + +export const AskUserQuestionSchema = z.object({ + index: z.number(), // 1-based + topic: z.string(), + question: z.string(), + options: z.array(z.string()), +}); + +export const AskUserRequestParamsSchema = z.object({ + /** The tool call id that initiated this AskUser request */ + toolCallId: z.string(), + questions: z.array(AskUserQuestionSchema), +}); + +export const AskUserRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidClientMethod.ASK_USER), + params: AskUserRequestParamsSchema, +}); + +export const AskUserCollectedAnswerSchema = z.object({ + index: z.number(), + question: z.string(), + answer: z.string(), +}); + +export const AskUserResultSchema = z.object({ + /** If true, the user cancelled the questionnaire */ + cancelled: z.boolean().optional(), + answers: z.array(AskUserCollectedAnswerSchema), +}); + +export const AskUserResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: AskUserResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const RequestPermissionResultSchema = z + .object({ + selectedOption: z.nativeEnum(ToolConfirmationOutcome), + comment: z.string().optional(), + editedSpecContent: z.string().optional(), + }) + .superRefine((value, ctx) => { + if ( + value.selectedOption === ToolConfirmationOutcome.ProceedEdit && + typeof value.editedSpecContent !== 'string' + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'editedSpecContent is required when selectedOption is proceed_edit', + path: ['editedSpecContent'], + }); + } + }); + +export const RequestPermissionResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: RequestPermissionResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const CliRequestOrNotificationSchema = JsonRpcEnvelopeSchema.and( + z.discriminatedUnion('method', [ + SessionNotificationSchema, + RequestPermissionRequestSchema, + AskUserRequestSchema, + ]) +); diff --git a/src/protocol/client.ts b/src/protocol/client.ts new file mode 100644 index 0000000..56e6393 --- /dev/null +++ b/src/protocol/client.ts @@ -0,0 +1,1104 @@ +// Source-of-truth mirror of factory-mono-alpha client protocol schemas. +// Verbatim copy of packages/common/src/droid/schemas/client.ts. + +import { z } from 'zod'; + +import { + AskUserRequestParamsSchema, + RequestPermissionRequestParamsSchema, + SandboxStatusSchema, +} from './cli.js'; +import { + AutonomyLevel, + AutonomyMode, + ContextCategoryColorKey, + ContextStatsAccuracy, + DecompSessionType, + DroidLocation, + DroidServerMethod, + LLMModelTier, + MessageRole, + MessageVisibility, + MissionState, + ModelKind, + ModelProvider, + QueuePlacement, + ReasoningEffort, + ResolveQueuedUserMessageAction, + SessionOrigin, + SettingsLevel, + SkillLocation, +} from './enums.js'; +import { HostIdSchema } from './host.js'; +import { + DroidInteractionModeSchema, + JsonRpcBaseRequestSchema, + JsonRpcBaseResponseFailureSchema, + JsonRpcBaseResponseSuccessSchema, +} from './json-rpc.js'; +import { LoopStateSchema } from './loop.js'; +import { + McpHttpServerConfigFieldsSchema, + McpRegistryServerSchema, + McpServerNameSchema, + McpServerStatusInfoSchema, + McpServerTypeSchema, + McpStatusSummarySchema, + McpStdioServerConfigFieldsSchema, + McpToolInfoSchema, +} from './mcp.js'; +import { + Base64ImageSourceSchema, + DocumentSourceSchema, + FactoryDroidMessageSchema, +} from './messages.js'; +import { + MissionFeatureSchema, + ProgressLogEntrySchema, +} from './mission-decomposition.js'; +import { MissionModelSettingsSchema } from './model-settings.js'; +import { SessionSourceSchema } from './session-source.js'; +import { SessionTagSchema, TokenUsageSchema } from './session.js'; + +/** + * Schema for available model configuration returned in session init/load responses. + * This represents both built-in models (feature-flag filtered) and custom BYOK models. + */ +export const AvailableModelConfigSchema = z.object({ + id: z.string(), + displayName: z.string(), + shortDisplayName: z.string(), + modelProvider: z.nativeEnum(ModelProvider), + supportedReasoningEfforts: z.array(z.nativeEnum(ReasoningEffort)), + defaultReasoningEffort: z.nativeEnum(ReasoningEffort), + isCustom: z.boolean().default(false), + noImageSupport: z.boolean().optional(), + tier: z.nativeEnum(LLMModelTier).optional(), + tokenMultiplier: z.number().optional(), + promoLabel: z.string().optional(), + kind: z.nativeEnum(ModelKind).optional(), + variantBadge: z.string().optional(), +}); + +export const ContextStatsSchema = z.object({ + used: z.number(), + remaining: z.number(), + limit: z.number(), + accuracy: z.nativeEnum(ContextStatsAccuracy), + updatedAt: z.string(), +}); + +const StdioMcpSchema = z.object({ + name: z.string(), + command: z.string(), + args: z.array(z.string()).default([]), + env: z.record(z.string()).optional().default({}), +}); + +const HttpHeaderSchema = z.object({ + name: z.string(), + value: z.string(), +}); + +const HttpMcpSchema = z.object({ + type: z.literal('http'), + name: z.string(), + url: z.string().url(), + headers: HttpHeaderSchema.array().default([]), +}); + +const SseMcpSchema = z.object({ + type: z.literal('sse'), + name: z.string(), + url: z.string().url(), + headers: HttpHeaderSchema.array().default([]), +}); + +const McpServersSchema = z + .union([StdioMcpSchema, HttpMcpSchema, SseMcpSchema]) + .array(); + +const SessionSchema = z.object({ + messages: FactoryDroidMessageSchema.array(), + title: z.string().optional(), +}); + +// Session type for mission decomposition (orchestrator manages workers) +const DecompSessionTypeSchema = z.nativeEnum(DecompSessionType); + +const ToolOverrideParamsSchema = z.object({ + enabledToolIds: z.array(z.string()).optional(), + disabledToolIds: z.array(z.string()).optional(), +}); + +export const OutputFormatSchema = z.object({ + type: z.literal('json_schema'), + schema: z.record(z.unknown()), +}); + +export const InitializeSessionRequestParamsSchema = z + .object({ + machineId: z.string(), + cwd: z.string(), + // Optional if we want to attach a session to a specific workspace + workspaceId: z.string().optional(), + // Optional if we want to create a session with a specific ID + sessionId: z.string().optional(), + mcpServers: McpServersSchema.optional(), + // Session settings (optional - uses defaults from settings.json if not provided) + autonomyMode: z + .nativeEnum(AutonomyMode) + .optional() + .describe('Deprecated: use interactionMode + autonomyLevel instead.'), + interactionMode: DroidInteractionModeSchema.optional().catch(undefined), + autonomyLevel: z.nativeEnum(AutonomyLevel).optional().catch(undefined), + modelId: z.string().optional(), + reasoningEffort: z.nativeEnum(ReasoningEffort).optional(), + specModeModelId: z.string().optional(), + specModeReasoningEffort: z.nativeEnum(ReasoningEffort).optional(), + missionSettings: MissionModelSettingsSchema.optional(), + compactionThresholdCheckEnabled: z.boolean().optional(), + /** + * @deprecated use session tags instead: mission decomposition session + * type (orchestrator or worker). undefined = standard session (not part of decomposition) + */ + decompSessionType: DecompSessionTypeSchema.optional(), + /** + * @deprecated use session tags instead: mission id for worker sessions + * (links worker back to its orchestrator's mission) + */ + decompMissionId: z.string().optional(), + // Skip permission checks (used by worker sessions running autonomously) + skipPermissionsUnsafe: z.boolean().optional(), + // Session metadata for delegations (Linear, Slack, etc.) + sessionLocation: z.string().optional(), + sessionSource: SessionSourceSchema.optional(), + sessionOriginHint: z.nativeEnum(SessionOrigin).optional(), + tags: z.array(SessionTagSchema).optional(), + // OAuth callback URI for MCP auth relay (set by frontend based on web vs desktop client) + mcpOAuthCallbackUri: z.string().optional(), + /** + * When true, the daemon should create or reuse a git worktree rooted + * at `cwd` and run the session there. The original `cwd` is used to + * locate the git repo; the actual session cwd becomes the worktree + * path. No-op when `cwd` is not inside a git repository. + */ + worktree: z.boolean().optional(), + /** + * Optional override for where the worktree directory is created. + * Falls back to the user's `worktreeDirectory` setting (or the + * sibling-of-repo default) when omitted. + */ + worktreeDir: z.string().optional(), + }) + .merge(ToolOverrideParamsSchema); + +export const LoadSessionRequestParamsSchema = z.object({ + sessionId: z.string(), + mcpServers: McpServersSchema.optional(), + loadAllMessages: z.boolean().optional(), + // OAuth callback URI for MCP auth relay (set by frontend based on web vs desktop client) + mcpOAuthCallbackUri: z.string().optional(), +}); + +export const AddUserMessageRequestParamsSchema = z.object({ + messageId: z.string().optional(), + text: z.string(), + images: Base64ImageSourceSchema.array().optional(), + files: DocumentSourceSchema.array().optional(), + outputFormat: OutputFormatSchema.optional(), + skipAgentLoop: z.boolean().optional(), + queuePlacement: z.nativeEnum(QueuePlacement).optional(), + role: z.nativeEnum(MessageRole).optional(), + visibility: z.nativeEnum(MessageVisibility).optional(), + userMessageSource: z.nativeEnum(SessionOrigin).optional(), +}); + +const QueuePlacementSchema = z.nativeEnum(QueuePlacement); + +export const ResolveQueuedUserMessageRequestParamsSchema = z.discriminatedUnion( + 'action', + [ + z.object({ + requestId: z.string(), + action: z.literal(ResolveQueuedUserMessageAction.UpdateQueue), + queuePlacement: QueuePlacementSchema, + }), + z.object({ + requestId: z.string(), + action: z.literal(ResolveQueuedUserMessageAction.Delete), + }), + ] +); + +export const InterruptSessionRequestParamsSchema = z.object({}); + +const CloseSessionRequestParamsSchema = z.object({ + reason: z.enum(['clear', 'logout', 'prompt_input_exit', 'other']).optional(), +}); + +export const CloseSessionResultSchema = z.object({}); + +export const KillWorkerSessionRequestParamsSchema = z.object({ + workerSessionId: z.string(), +}); + +export const KillWorkerSessionResultSchema = z.object({}); + +export const UpdateSessionSettingsRequestParamsSchema = z + .object({ + modelId: z.string().optional(), + reasoningEffort: z.nativeEnum(ReasoningEffort).optional(), + autonomyMode: z + .nativeEnum(AutonomyMode) + .optional() + .describe('Deprecated: use interactionMode + autonomyLevel instead.'), + interactionMode: DroidInteractionModeSchema.optional().catch(undefined), + autonomyLevel: z.nativeEnum(AutonomyLevel).optional().catch(undefined), + specModeModelId: z.string().nullable().optional(), + specModeReasoningEffort: z + .nativeEnum(ReasoningEffort) + .nullable() + .optional(), + missionSettings: MissionModelSettingsSchema.optional(), + tags: z.array(SessionTagSchema).optional(), + compactionTokenLimit: z.number().optional(), + compactionThresholdCheckEnabled: z.boolean().optional(), + }) + .merge(ToolOverrideParamsSchema); + +const SessionSettingsSchema = z + .object({ + modelId: z.string(), + reasoningEffort: z.nativeEnum(ReasoningEffort), + autonomyMode: z + .nativeEnum(AutonomyMode) + .optional() + .describe('Deprecated: use interactionMode + autonomyLevel instead.'), + interactionMode: DroidInteractionModeSchema.optional().catch(undefined), + autonomyLevel: z.nativeEnum(AutonomyLevel).optional().catch(undefined), + specModeModelId: z.string().optional(), + specModeReasoningEffort: z.nativeEnum(ReasoningEffort).optional(), + missionSettings: MissionModelSettingsSchema.optional(), + tags: z.array(SessionTagSchema).optional(), + sandbox: SandboxStatusSchema.optional(), + compactionThresholdCheckEnabled: z.boolean().optional(), + }) + .merge(ToolOverrideParamsSchema); + +const GitRepoInfoSchema = z.object({ + owner: z.string().optional(), + repoName: z.string(), +}); + +/** + * Worktree metadata returned to the client when a session is created in + * an isolated git worktree. Surfaces the branch + path so the UI can + * display badges and the user can locate the worktree on disk. + */ +const SessionWorktreeInfoSchema = z.object({ + branch: z.string(), + path: z.string(), + repoRoot: z.string().optional(), + isNewlyCreated: z.boolean(), +}); + +export const InitializeSessionResultSchema = z.object({ + sessionId: z.string(), + hostId: HostIdSchema.optional(), + session: SessionSchema, + mcpServers: McpServersSchema.optional(), + settings: SessionSettingsSchema, + gitRepo: GitRepoInfoSchema.optional(), + availableModels: z.array(AvailableModelConfigSchema).optional(), + worktree: SessionWorktreeInfoSchema.optional(), +}); + +export const WorkerStateInfoSchema = z.object({ + startedAt: z.string(), + completedAt: z.string().optional(), + exitCode: z.number().optional(), +}); + +// Mission snapshot schema (optional, only for orchestrator sessions) +export const MissionStateSchema = z.object({ + state: z.nativeEnum(MissionState), + updatedAt: z.string().optional(), + title: z.string().optional(), + workingDirectory: z.string().optional(), + features: z.array(MissionFeatureSchema), + progressLog: z.array(ProgressLogEntrySchema), + workerSessionIds: z.array(z.string()), + workerStates: z.record(WorkerStateInfoSchema).optional(), + tokenUsage: TokenUsageSchema.optional(), + tokenUsageBySessionId: z.record(TokenUsageSchema).optional(), +}); + +export const LoadSessionResultSchema = z.object({ + session: SessionSchema, + hostId: HostIdSchema.optional(), + mcpServers: McpServersSchema.optional(), + pendingPermissions: z + .array( + RequestPermissionRequestParamsSchema.extend({ + requestId: z.string(), + }) + ) + .optional(), + pendingAskUserRequests: z + .array( + AskUserRequestParamsSchema.extend({ + requestId: z.string(), + }) + ) + .optional(), + settings: SessionSettingsSchema, + isAgentLoopInProgress: z.boolean().optional(), + queuedMessages: z + .array( + AddUserMessageRequestParamsSchema.extend({ + requestId: z.string(), + }) + ) + .optional(), + gitRepo: GitRepoInfoSchema.optional(), + cwd: z.string().optional(), + callingSessionId: z.string().optional(), + callingToolUseId: z.string().optional(), + availableModels: z.array(AvailableModelConfigSchema).optional(), + // Token usage for the session (input, output, cache tokens) + tokenUsage: TokenUsageSchema.optional(), + // Mission state (only for orchestrator sessions with active missions) + mission: MissionStateSchema.optional(), + // Session type for mission decomposition (orchestrator or worker) + decompSessionType: DecompSessionTypeSchema.optional(), + loopState: LoopStateSchema.nullable().optional(), +}); + +export const AddUserMessageResultSchema = z.object({}); + +export const ResolveQueuedUserMessageResultSchema = z.object({}); + +export const InterruptSessionResultSchema = z.object({}); + +export const UpdateSessionSettingsResultSchema = z.object({}); + +export const GetUserInfoResultSchema = z.object({ + userId: z.string(), + orgId: z.string(), +}); + +export const ValidateWorkingDirectoryResultSchema = z.object({ + isValid: z.boolean(), + error: z.string().optional(), + /** + * The fully-resolved absolute path (tilde expanded, `..`/`.` normalized, + * symlinks canonicalized). Set by daemons that support it when + * `isValid === true`. Optional for backwards compatibility with daemons + * that predate the field — callers should fall back to the original + * user input when absent. + */ + resolvedPath: z.string().optional(), +}); + +export const InitializeSessionRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.INITIALIZE_SESSION), + params: InitializeSessionRequestParamsSchema, +}); + +export const LoadSessionRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.LOAD_SESSION), + params: LoadSessionRequestParamsSchema, +}); + +export const AddUserMessageRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.ADD_USER_MESSAGE), + params: AddUserMessageRequestParamsSchema, +}); + +export const ResolveQueuedUserMessageRequestSchema = + JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.RESOLVE_QUEUED_USER_MESSAGE), + params: ResolveQueuedUserMessageRequestParamsSchema, + }); + +export const InterruptSessionRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.INTERRUPT_SESSION), + params: InterruptSessionRequestParamsSchema, +}); + +export const CloseSessionRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.CLOSE_SESSION), + params: CloseSessionRequestParamsSchema, +}); + +export const KillWorkerSessionRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.KILL_WORKER_SESSION), + params: KillWorkerSessionRequestParamsSchema, +}); + +export const UpdateSessionSettingsRequestSchema = + JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.UPDATE_SESSION_SETTINGS), + params: UpdateSessionSettingsRequestParamsSchema, + }); + +// MCP server management requests +const McpServerNameParamsSchema = z.object({ + serverName: McpServerNameSchema, +}); + +// MCP config mutations are applied via user-level overrides only. +const McpSettingsLevelSchema = z.literal(SettingsLevel.User); + +const McpHttpServerAddFieldsSchema = McpHttpServerConfigFieldsSchema.extend({ + headers: z.record(z.string()).optional(), +}); + +const McpStdioServerAddFieldsSchema = McpStdioServerConfigFieldsSchema.extend({ + env: z.record(z.string()).optional(), +}); + +export const ToggleMcpServerRequestParamsSchema = + McpServerNameParamsSchema.extend({ + enabled: z.boolean(), + settingsLevel: McpSettingsLevelSchema, + }); + +const ToggleMcpServerResultSchema = z.object({ + success: z.boolean(), +}); + +export const ToggleMcpServerRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.TOGGLE_MCP_SERVER), + params: ToggleMcpServerRequestParamsSchema, +}); + +export const AuthenticateMcpServerRequestParamsSchema = + McpServerNameParamsSchema.extend({}); + +const AuthenticateMcpServerResultSchema = z.object({ + success: z.boolean(), +}); + +export const AuthenticateMcpServerRequestSchema = + JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.AUTHENTICATE_MCP_SERVER), + params: AuthenticateMcpServerRequestParamsSchema, + }); + +export const CancelMcpAuthRequestParamsSchema = + McpServerNameParamsSchema.extend({}); + +const CancelMcpAuthResultSchema = z.object({ + success: z.boolean(), +}); + +export const CancelMcpAuthRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.CANCEL_MCP_AUTH), + params: CancelMcpAuthRequestParamsSchema, +}); + +export const ClearMcpAuthRequestParamsSchema = McpServerNameParamsSchema.extend( + {} +); + +const ClearMcpAuthResultSchema = z.object({ + success: z.boolean(), +}); + +export const ClearMcpAuthRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.CLEAR_MCP_AUTH), + params: ClearMcpAuthRequestParamsSchema, +}); + +// Submit MCP auth code (for remote sessions) +export const SubmitMcpAuthCodeRequestParamsSchema = z.object({ + serverName: McpServerNameSchema, + code: z.string(), + state: z.string(), +}); + +const SubmitMcpAuthCodeResultSchema = z.object({ + success: z.boolean(), +}); + +export const SubmitMcpAuthCodeRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.SUBMIT_MCP_AUTH_CODE), + params: SubmitMcpAuthCodeRequestParamsSchema, +}); + +export const SubmitMcpAuthCodeResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: SubmitMcpAuthCodeResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +// Add MCP server request +export const AddMcpServerRequestParamsSchema = z + .object({ + name: McpServerNameSchema, + type: McpServerTypeSchema, + }) + .merge(McpHttpServerAddFieldsSchema) + .merge(McpStdioServerAddFieldsSchema); + +const AddMcpServerResultSchema = z.object({ + success: z.boolean(), +}); + +export const AddMcpServerRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.ADD_MCP_SERVER), + params: AddMcpServerRequestParamsSchema, +}); + +// Remove MCP server request +export const RemoveMcpServerRequestParamsSchema = + McpServerNameParamsSchema.extend({ + settingsLevel: McpSettingsLevelSchema, + }); + +const RemoveMcpServerResultSchema = z.object({ + success: z.boolean(), +}); + +export const RemoveMcpServerRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.REMOVE_MCP_SERVER), + params: RemoveMcpServerRequestParamsSchema, +}); + +// List MCP registry request (returns available servers from hardcoded registry) +export const ListMcpRegistryRequestParamsSchema = z.object({}); + +export const ListMcpRegistryResultSchema = z.object({ + servers: z.array(McpRegistryServerSchema), +}); + +export const ListMcpRegistryRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.LIST_MCP_REGISTRY), + params: ListMcpRegistryRequestParamsSchema, +}); + +// List MCP tools request (returns all tools with enabled/disabled state) +export const ListMcpToolsRequestParamsSchema = z.object({}); + +export const ListMcpToolsResultSchema = z.object({ + tools: z.array(McpToolInfoSchema), +}); + +export const ListMcpToolsRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.LIST_MCP_TOOLS), + params: ListMcpToolsRequestParamsSchema, +}); + +const ToolCatalogCategorySchema = z.enum(['read', 'edit', 'execute', 'other']); + +const ExecToolInfoSchema = z.object({ + id: z.string(), + llmId: z.string(), + displayName: z.string(), + description: z.string(), + category: ToolCatalogCategorySchema, + defaultAllowed: z.boolean(), + currentlyAllowed: z.boolean(), +}); + +export const ListToolsRequestParamsSchema = + UpdateSessionSettingsRequestParamsSchema.pick({ + modelId: true, + autonomyMode: true, + interactionMode: true, + autonomyLevel: true, + specModeModelId: true, + enabledToolIds: true, + disabledToolIds: true, + }).extend({ + skipPermissionsUnsafe: z.boolean().optional(), + depth: z.number().int().min(0).optional(), + }); + +export const ListToolsResultSchema = z.object({ + tools: z.array(ExecToolInfoSchema), +}); + +export const ListToolsRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.LIST_TOOLS), + params: ListToolsRequestParamsSchema, +}); + +// List MCP servers request (returns current server status, same shape as MCP_STATUS_CHANGED notification) +export const ListMcpServersRequestParamsSchema = z.object({}); + +export const ListMcpServersResultSchema = z.object({ + servers: z.array(McpServerStatusInfoSchema), + summary: McpStatusSummarySchema, +}); + +export const ListMcpServersRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.LIST_MCP_SERVERS), + params: ListMcpServersRequestParamsSchema, +}); + +// Toggle MCP tool request (enable/disable a specific tool) +export const ToggleMcpToolRequestParamsSchema = + McpServerNameParamsSchema.extend({ + toolName: z.string(), + enabled: z.boolean(), + }); + +const ToggleMcpToolResultSchema = z.object({ + success: z.boolean(), +}); + +export const ToggleMcpToolRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.TOGGLE_MCP_TOOL), + params: ToggleMcpToolRequestParamsSchema, +}); + +// List skills request (returns all available skills) +const ListSkillsRequestParamsSchema = z.object({}); + +// Resource file in a skill folder (anything except SKILL.md) +const SkillResourceSchema = z.object({ + name: z.string(), + path: z.string(), + type: z.enum(['reference', 'asset']), // reference = .md files, asset = other files +}); + +export const SkillInfoSchema = z.object({ + name: z.string(), + description: z.string().optional(), + location: z.nativeEnum(SkillLocation), + filePath: z.string(), + enabled: z.boolean().optional(), + userInvocable: z.boolean().optional(), + version: z.string().optional(), + // Full SKILL.md content (markdown) + content: z.string().optional(), + // Other files in the skill folder + resources: z.array(SkillResourceSchema).optional(), +}); + +const ListSkillsResultSchema = z.object({ + skills: z.array(SkillInfoSchema), +}); + +export const ListSkillsRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.LIST_SKILLS), + params: ListSkillsRequestParamsSchema, +}); + +const GetContextStatsRequestParamsSchema = z.object({}); + +export { ContextStatsSchema as GetContextStatsResultSchema }; + +export const GetContextStatsRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.GET_CONTEXT_STATS), + params: GetContextStatsRequestParamsSchema, +}); + +const ContextBreakdownCategorySchema = z.object({ + name: z.string(), + tokens: z.number(), + colorKey: z.nativeEnum(ContextCategoryColorKey), +}); + +const ContextBreakdownSkillEntrySchema = z.object({ + name: z.string(), + location: z.nativeEnum(SkillLocation), + tokens: z.number(), +}); + +const ContextBreakdownMcpServerEntrySchema = z.object({ + name: z.string(), + toolCount: z.number(), + tokens: z.number(), +}); + +const ContextBreakdownDroidEntrySchema = z.object({ + name: z.string(), + location: z.nativeEnum(DroidLocation), + tokens: z.number(), +}); + +const GetContextBreakdownRequestParamsSchema = z.object({}); + +export const GetContextBreakdownResultSchema = z.object({ + modelId: z.string(), + modelDisplayName: z.string(), + contextBudget: z.number(), + lastCallCompactionTokens: z.number().optional(), + usedTokens: z.number(), + freeTokens: z.number(), + categories: z.array(ContextBreakdownCategorySchema), + skills: z.array(ContextBreakdownSkillEntrySchema), + mcpServers: z.array(ContextBreakdownMcpServerEntrySchema), + droids: z.array(ContextBreakdownDroidEntrySchema), +}); + +export const GetContextBreakdownRequestSchema = JsonRpcBaseRequestSchema.extend( + { + method: z.literal(DroidServerMethod.GET_CONTEXT_BREAKDOWN), + params: GetContextBreakdownRequestParamsSchema, + } +); + +// Submit bug report request (creates and uploads bug report with session data) +const SubmitBugReportRequestParamsSchema = z.object({ + userComment: z.string(), + clientLogs: z.string().optional(), +}); + +export const SubmitBugReportResultSchema = z.object({ + bugReportId: z.string(), +}); + +export const SubmitBugReportRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.SUBMIT_BUG_REPORT), + params: SubmitBugReportRequestParamsSchema, +}); + +// Rewind schemas + +const GetRewindInfoRequestParamsSchema = z.object({ + sessionId: z.string(), + messageId: z.string(), +}); + +const RewindFileSnapshotSchema = z.object({ + filePath: z.string(), + contentHash: z.string(), + size: z.number(), +}); + +const RewindFileCreationSchema = z.object({ + filePath: z.string(), +}); + +const RewindEvictedFileSchema = z.object({ + filePath: z.string(), + reason: z.string(), +}); + +export const GetRewindInfoResultSchema = z.object({ + availableFiles: z.array(RewindFileSnapshotSchema), + createdFiles: z.array(RewindFileCreationSchema), + evictedFiles: z.array(RewindEvictedFileSchema), +}); + +export const GetRewindInfoRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.GET_REWIND_INFO), + params: GetRewindInfoRequestParamsSchema, +}); + +const ExecuteRewindRequestParamsSchema = z.object({ + sessionId: z.string(), + messageId: z.string(), + filesToRestore: z.array(RewindFileSnapshotSchema), + filesToDelete: z.array(RewindFileCreationSchema), + forkTitle: z.string(), +}); + +export const ExecuteRewindResultSchema = z.object({ + newSessionId: z.string(), + restoredCount: z.number(), + deletedCount: z.number(), + failedRestoreCount: z.number(), + failedDeleteCount: z.number(), +}); + +export const ExecuteRewindRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.EXECUTE_REWIND), + params: ExecuteRewindRequestParamsSchema, +}); + +// Compact session schemas + +const CompactSessionRequestParamsSchema = z.object({ + customInstructions: z.string().optional(), +}); + +export const CompactSessionResultSchema = z.object({ + newSessionId: z.string(), + removedCount: z.number(), +}); + +export const CompactSessionRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.COMPACT_SESSION), + params: CompactSessionRequestParamsSchema, +}); + +export const CompactSessionResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: CompactSessionResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ForkSessionResultSchema = z.object({ + newSessionId: z.string(), +}); + +export const ForkSessionRequestParamsSchema = z.object({ + title: z.string().optional(), + tags: z + .array( + z.object({ name: z.string(), metadata: z.record(z.string()).optional() }) + ) + .optional(), +}); + +export const ForkSessionRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.FORK_SESSION), + params: ForkSessionRequestParamsSchema, +}); + +export const ForkSessionResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ForkSessionResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +// Rename Session +export const RenameSessionRequestParamsSchema = z.object({ + title: z.string(), +}); + +export const RenameSessionResultSchema = z.object({ + success: z.boolean(), +}); + +export const RenameSessionRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.RENAME_SESSION), + params: RenameSessionRequestParamsSchema, +}); + +export const RenameSessionResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: RenameSessionResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +// Cache warmup +export const WarmupCacheRequestSchema = JsonRpcBaseRequestSchema.extend({ + method: z.literal(DroidServerMethod.WARMUP_CACHE), + params: z.object({}), +}); + +export const WarmupCacheResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: z.object({}), + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ClientRequestSchema = z.discriminatedUnion('method', [ + InitializeSessionRequestSchema, + LoadSessionRequestSchema, + InterruptSessionRequestSchema, + CloseSessionRequestSchema, + KillWorkerSessionRequestSchema, + AddUserMessageRequestSchema, + ResolveQueuedUserMessageRequestSchema, + UpdateSessionSettingsRequestSchema, + ToggleMcpServerRequestSchema, + AuthenticateMcpServerRequestSchema, + CancelMcpAuthRequestSchema, + ClearMcpAuthRequestSchema, + AddMcpServerRequestSchema, + RemoveMcpServerRequestSchema, + ListMcpRegistryRequestSchema, + ListMcpToolsRequestSchema, + ListToolsRequestSchema, + ListMcpServersRequestSchema, + ToggleMcpToolRequestSchema, + SubmitMcpAuthCodeRequestSchema, + ListSkillsRequestSchema, + GetContextStatsRequestSchema, + GetContextBreakdownRequestSchema, + SubmitBugReportRequestSchema, + GetRewindInfoRequestSchema, + ExecuteRewindRequestSchema, + CompactSessionRequestSchema, + ForkSessionRequestSchema, + RenameSessionRequestSchema, + WarmupCacheRequestSchema, +]); + +export const InitializeSessionResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: InitializeSessionResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const LoadSessionResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: LoadSessionResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const AddUserMessageResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: AddUserMessageResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ResolveQueuedUserMessageResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ResolveQueuedUserMessageResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const InterruptSessionResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: InterruptSessionResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const CloseSessionResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: CloseSessionResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const KillWorkerSessionResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: KillWorkerSessionResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const UpdateSessionSettingsResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: UpdateSessionSettingsResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ToggleMcpServerResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ToggleMcpServerResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const AuthenticateMcpServerResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: AuthenticateMcpServerResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const CancelMcpAuthResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: CancelMcpAuthResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ClearMcpAuthResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ClearMcpAuthResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const AddMcpServerResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: AddMcpServerResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const RemoveMcpServerResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: RemoveMcpServerResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ListMcpRegistryResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ListMcpRegistryResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ListMcpToolsResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ListMcpToolsResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ListToolsResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ListToolsResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ListMcpServersResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ListMcpServersResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ToggleMcpToolResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ToggleMcpToolResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ListSkillsResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ListSkillsResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const GetContextStatsResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ContextStatsSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const GetContextBreakdownResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: GetContextBreakdownResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const SubmitBugReportResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: SubmitBugReportResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const GetRewindInfoResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: GetRewindInfoResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); + +export const ExecuteRewindResponseSchema = z.union([ + JsonRpcBaseResponseSuccessSchema.extend({ + result: ExecuteRewindResultSchema, + }), + JsonRpcBaseResponseFailureSchema, +]); diff --git a/src/protocol/custom-models.ts b/src/protocol/custom-models.ts new file mode 100644 index 0000000..882721a --- /dev/null +++ b/src/protocol/custom-models.ts @@ -0,0 +1,85 @@ +// Source-of-truth mirror of factory-mono-alpha custom-model schemas (symbol-only). +// Verbatim copy of CustomModelBedrockSchema, ManagedCustomModelSchema, and +// CustomModelsSchema from packages/common/src/settings/schema.ts. Ported as-is +// per the no-modification rule (includes apiKey / aws credential fields). +// SandboxModeSchema is colocated here (one-liner from the same settings module). + +import { z } from 'zod'; + +import { ModelProvider, ReasoningEffort, SandboxMode } from './enums.js'; + +const ReasoningEffortSchema = z.nativeEnum(ReasoningEffort); + +export const SandboxModeSchema = z.nativeEnum(SandboxMode); + +const CustomModelBedrockSchema = z.object({ + awsProfile: z.string().optional(), + awsRegion: z.string().min(1).optional(), + bedrockBaseUrl: z.string().url().optional(), + awsAuthRefresh: z.string().optional(), + awsCredentialExport: z.string().optional(), +}); + +export const ManagedCustomModelSchema = z + .object({ + model: z.string(), + id: z.string().optional(), + index: z.number().optional(), + baseUrl: z.string().optional(), + apiKey: z.string().optional(), + provider: z.nativeEnum(ModelProvider), + displayName: z.string().optional(), + maxContextLimit: z.number().optional(), + enableThinking: z.boolean().optional(), + thinkingMaxTokens: z.number().optional(), + maxOutputTokens: z.number().optional(), + reasoningEffort: ReasoningEffortSchema.optional(), + extraHeaders: z.record(z.string()).optional(), + extraArgs: z.record(z.unknown()).optional(), + noImageSupport: z.boolean().optional(), + bedrock: CustomModelBedrockSchema.optional(), + }) + .superRefine((model, ctx) => { + if (model.provider === ModelProvider.BEDROCK_CONVERSE && !model.bedrock) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['bedrock'], + message: + 'bedrock-converse custom models require a bedrock configuration block', + }); + return; + } + if (model.bedrock) { + if ( + model.provider !== ModelProvider.ANTHROPIC && + model.provider !== ModelProvider.BEDROCK_CONVERSE + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['provider'], + message: + 'bedrock custom models require provider "anthropic" or "bedrock-converse"', + }); + } + return; + } + if (!model.baseUrl) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['baseUrl'], + message: 'baseUrl is required unless bedrock is configured', + }); + } + if (!model.apiKey) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['apiKey'], + message: 'apiKey is required unless bedrock is configured', + }); + } + }); + +export const CustomModelsSchema = z.array(ManagedCustomModelSchema); + +export type ManagedCustomModel = z.infer; +export type CustomModels = z.infer; diff --git a/src/protocol/enums.ts b/src/protocol/enums.ts index 345dfbc..561425e 100644 --- a/src/protocol/enums.ts +++ b/src/protocol/enums.ts @@ -585,3 +585,50 @@ export enum ApiProvider { BASETEN = 'baseten', SNOWFLAKE = 'snowflake', } + +export enum ModelKind { + Concrete = 'concrete', + Router = 'router', +} + +export enum LLMModelTier { + Standard = 'standard', + // Deprecated + Premium = 'premium', + // Extra Usage (overage) billing tier + Overage = 'overage', +} + +// --------------------------------------------------------------------------- +// settings/enums.ts (leaf subset) +// --------------------------------------------------------------------------- + +/** + * Settings hierarchy level enum. + * Precedence order (highest to lowest): Org -> Runtime -> Folder -> Project -> User + */ +export enum SettingsLevel { + Org = 'org', + Runtime = 'runtime', + User = 'user', + Project = 'project', + Folder = 'folder', + Dynamic = 'dynamic', + BuiltIn = 'builtin', +} + +export enum DroidLocation { + Project = 'project', + Personal = 'personal', +} + +export enum SkillLocation { + Project = 'project', + Personal = 'personal', + Builtin = 'builtin', +} + +export enum SandboxMode { + PerCommand = 'per-command', + WholeProcess = 'whole-process', +} diff --git a/src/protocol/index.ts b/src/protocol/index.ts index cffe8ad..9be1fba 100644 --- a/src/protocol/index.ts +++ b/src/protocol/index.ts @@ -13,3 +13,8 @@ export * from './messages.js'; export * from './model-settings.js'; export * from './loop.js'; export * from './selectable-list-item.js'; +export * from './custom-models.js'; +export * from './mcp.js'; +export * from './mission-decomposition.js'; +export * from './cli.js'; +export * from './client.js'; diff --git a/src/protocol/mcp.ts b/src/protocol/mcp.ts new file mode 100644 index 0000000..de3e847 --- /dev/null +++ b/src/protocol/mcp.ts @@ -0,0 +1,77 @@ +// Source-of-truth mirror of factory-mono-alpha MCP schemas. +// Verbatim copy of packages/common/src/droid/schemas/mcp.ts. + +import { z } from 'zod'; + +import { McpServerStatus, McpServerType, SettingsLevel } from './enums.js'; + +// MCP primitives +export const McpServerNameSchema = z.string(); +export const McpServerTypeSchema = z.enum(['stdio', 'http', 'sse']); + +// Server config field schemas (used by registry and add-server requests) +export const McpHttpServerConfigFieldsSchema = z.object({ + url: z.string().optional(), +}); + +export const McpStdioServerConfigFieldsSchema = z.object({ + command: z.string().optional(), + args: z.array(z.string()).optional(), +}); + +// MCP server status (shared between LIST_MCP_SERVERS result and MCP_STATUS_CHANGED notification) +export const McpServerStatusInfoSchema = z.object({ + name: z.string(), + status: z.nativeEnum(McpServerStatus), + source: z.nativeEnum(SettingsLevel), + isManaged: z.boolean(), + error: z.string().optional(), + toolCount: z.number().optional(), + serverType: z.nativeEnum(McpServerType), + hasAuthTokens: z.boolean().optional(), + requiresAuth: z.boolean().optional(), + pendingAuthUrl: z.string().optional(), + pendingAuthMessage: z.string().optional(), + pendingAuthState: z.string().optional(), +}); + +// MCP status summary (shared between LIST_MCP_SERVERS result and MCP_STATUS_CHANGED notification) +export const McpStatusSummarySchema = z.object({ + total: z.number(), + connected: z.number(), + connecting: z.number(), + failed: z.number(), + disabled: z.number().optional(), +}); + +// MCP registry server entity +const McpRegistryServerBaseSchema = z.object({ + name: McpServerNameSchema, + description: z.string(), + type: McpServerTypeSchema, +}); + +export const McpRegistryServerSchema = McpRegistryServerBaseSchema.merge( + McpHttpServerConfigFieldsSchema +) + .merge(McpStdioServerConfigFieldsSchema) + .extend({ + note: z.string().optional(), + logoUrl: z.string().optional(), + }); + +// MCP tool entity +export const McpToolInfoSchema = z.object({ + serverName: McpServerNameSchema, + name: z.string(), + description: z.string().optional(), + isEnabled: z.boolean(), + isReadOnly: z.boolean().optional(), + inputSchema: z + .object({ + type: z.string().optional(), + properties: z.record(z.unknown()).optional(), + required: z.array(z.string()).optional(), + }) + .optional(), +}); diff --git a/src/protocol/mission-decomposition.ts b/src/protocol/mission-decomposition.ts new file mode 100644 index 0000000..2ce5053 --- /dev/null +++ b/src/protocol/mission-decomposition.ts @@ -0,0 +1,346 @@ +// Source-of-truth mirror of factory-mono-alpha mission-decomposition schemas. +// Verbatim copy of packages/common/src/droid/schemas/mission-decomposition.ts. + +import { z } from 'zod'; + +import { CustomModelsSchema } from './custom-models.js'; +import { + DismissalType, + FeatureStatus, + FeatureSuccessState, + IssueSeverity, + MissionPauseReason, + MissionState, + ProgressLogEntryType, + WorkerFailureReason, +} from './enums.js'; + +export const FeatureSuccessStateSchema = z.nativeEnum(FeatureSuccessState); + +const OptionalNonBlankStringSchema = z.preprocess( + (value) => + typeof value === 'string' && value.trim() === '' ? undefined : value, + z.string().optional() +); + +// ------------------------- +// Feature schema +// ------------------------- + +/** + * Mission feature schema. + * + * NOTE: This is intentionally tolerant (passthrough + optional fields) + * because the CLI/orchestrator owns the canonical on-disk feature shape and + * may evolve it over time. + */ +export const MissionFeatureSchema = z.object({ + id: z.string(), + description: z.string(), + status: z.nativeEnum(FeatureStatus), + skillName: z.string(), + preconditions: z.array(z.string()), + expectedBehavior: z.array(z.string()), + fulfills: z.array(z.string()).optional(), + + // Optional orchestrator-authored fields + milestone: z.string().optional(), + + // System-managed fields (present in CLI features.json) + workerSessionIds: z.array(z.string()).optional(), + // Deprecated compatibility fields kept for protocol stability. + currentWorkerSessionId: z.string().nullable().optional(), + completedWorkerSessionId: z.string().nullable().optional(), +}); + +// ------------------------- +// Progress log schemas +// ------------------------- + +// ------------------------- +// Handoff-related schemas +// ------------------------- + +const IssueSeveritySchema = z.nativeEnum(IssueSeverity); + +const DismissalTypeSchema = z.nativeEnum(DismissalType); + +export const DiscoveredIssueSchema = z.object({ + severity: IssueSeveritySchema, + description: z.string(), + suggestedFix: z.string().optional(), +}); + +export const VerificationCommandSchema = z.object({ + command: z.string(), + exitCode: z.number(), + observation: z.string(), +}); + +export const InteractiveCheckSchema = z.object({ + action: z.string(), + observed: z.string(), +}); + +export const VerificationSchema = z.object({ + commandsRun: z.array(VerificationCommandSchema), + interactiveChecks: z.array(InteractiveCheckSchema).optional(), +}); + +export const TestCaseSchema = z.object({ + name: z.string(), + verifies: z.string(), +}); + +export const TestFileSchema = z.object({ + file: z.string(), + cases: z.array(TestCaseSchema), +}); + +export const TestsSchema = z.object({ + added: z.array(TestFileSchema), + updated: z.array(z.string()).optional(), + coverage: z.string(), +}); + +const SkillDeviationSchema = z.object({ + step: z.string(), + whatIDidInstead: z.string(), + why: z.string(), +}); + +const SkillFeedbackSchema = z.object({ + followedProcedure: z.boolean(), + deviations: z.array(SkillDeviationSchema), + suggestedChanges: z.array(z.string()).optional(), +}); + +export const HandoffSchema = z.object({ + // Optional for backward compatibility with historical on-disk handoffs. + salientSummary: z.string().optional(), + whatWasImplemented: z.string(), + whatWasLeftUndone: z.string(), + verification: VerificationSchema, + tests: TestsSchema, + discoveredIssues: z.array(DiscoveredIssueSchema), + skillFeedback: SkillFeedbackSchema.optional(), +}); + +export const DismissalRecordSchema = z.object({ + type: DismissalTypeSchema, + sourceFeatureId: z.string(), + summary: z.string(), + justification: z.string(), +}); + +// ------------------------- +// On-disk mission artifact schemas (GitHub issue #974) +// +// These describe the shape of files the CLI's MissionFileService reads +// and writes under `{missionDir}/`. They are intentionally stricter than +// the protocol-level MissionFeatureSchema above: on-disk corruption +// (a missing `status`, a truncated `state.json`, etc.) is the failure +// class that drives scheduler-replay bugs, so schema enforcement here is +// the first line of defense. +// ------------------------- + +/** + * On-disk feature schema. Kept intentionally more permissive than + * `MissionFeatureSchema` (several list-typed fields are optional here + * because `MissionFileService.normalizeFeature()` fills them with `[]` + * defaults after reading). The fields we keep strict are the ones the + * orchestrator always authors and whose silent regression would cause + * scheduler-replay bugs: `id`, `description`, `status`. + */ +const OnDiskMissionFeatureSchema = z + .object({ + id: z.string(), + description: z.string(), + // Required: silent default-to-Pending re-enables the replay bug #974 + // defends against. Missing status is corruption. Readers may have a + // narrow self-heal path for uniformly-legacy files authored by older + // CLI builds that never wrote `status`; anything else routes through + // the pause/repair flow in MissionRunner. + status: z.nativeEnum(FeatureStatus), + skillName: z.string().optional(), + preconditions: z.array(z.string()).optional(), + expectedBehavior: z.union([z.array(z.string()), z.string()]).optional(), + fulfills: z.array(z.string()).optional(), + milestone: z.string().optional(), + workerSessionIds: z.array(z.string()).optional(), + currentWorkerSessionId: z.string().nullable().optional(), + completedWorkerSessionId: z.string().nullable().optional(), + }) + .passthrough(); + +/** + * Schema for handoff entries stored in `handoffs.jsonl`. + */ +export const HandoffEntrySchema = z.object({ + timestamp: z.string(), + workerSessionId: z.string(), + featureId: z.string(), + milestone: z.string().optional(), + commitId: OptionalNonBlankStringSchema, + repoPath: OptionalNonBlankStringSchema, + handoff: HandoffSchema, +}); + +/** + * Schema for per-worker handoff JSON files written to + * `{missionDir}/handoffs/{timestamp}__{featureId}__{workerSessionId}.json`. + */ +export const WorkerHandoffFileSchema = z.object({ + timestamp: z.string(), + workerSessionId: z.string(), + featureId: z.string(), + milestone: z.string().optional(), + commitId: z.string().optional(), + successState: z.string().optional(), + returnToOrchestrator: z.boolean().optional(), + handoff: HandoffSchema, +}); + +/** + * Schema for the mission state file (`state.json`). System-managed. + * + * Kept permissive on unknown top-level keys (`.passthrough`) to stay + * forward-compatible with in-flight missions authored by older CLI versions, + * but strict on required fields and enums so that genuine corruption (e.g. + * truncated write) is caught. + */ +export const MissionStateFileSchema = z + .object({ + missionId: z.string().min(1), + state: z.nativeEnum(MissionState), + workingDirectory: z.string().min(1), + createdAt: z.string(), + updatedAt: z.string(), + lastReviewedHandoffCount: z.number().nonnegative().optional(), + }) + .passthrough(); + +/** + * Schema for the features file (`features.json`). + * + * Accepts only the modern `{ "features": [...] }` wrapper shape; legacy bare + * arrays are normalized into the wrapper shape by callers before validation. + */ +export const FeaturesFileSchema = z + .object({ + features: z.array(OnDiskMissionFeatureSchema), + }) + .passthrough(); + +/** + * Schema for `runtime-custom-models.json`. + */ +export const RuntimeCustomModelsFileSchema = z.object({ + customModels: CustomModelsSchema, +}); + +// ------------------------- +// Progress log entry schemas +// ------------------------- + +const BaseProgressLogEntrySchema = z.object({ + timestamp: z.string(), +}); + +export const MissionAcceptedEntrySchema = BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.MissionAccepted), + title: z.string(), +}); + +export const MissionPausedEntrySchema = BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.MissionPaused), + /** + * Structured cause when the pause is automatic (e.g. unrecoverable 402). + * Absent for user- or runner-initiated pauses. + */ + pauseReason: z.nativeEnum(MissionPauseReason).optional(), +}); + +export const MissionResumedEntrySchema = BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.MissionResumed), + resumeWorkerSessionId: z.string().optional(), +}); + +export const MissionRunStartedEntrySchema = BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.MissionRunStarted), + message: z.string().optional(), +}); + +export const WorkerStartedEntrySchema = BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.WorkerStarted), + workerSessionId: z.string(), + spawnId: z.string(), + featureId: z.string().optional(), +}); + +export const WorkerSelectedFeatureEntrySchema = + BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.WorkerSelectedFeature), + workerSessionId: z.string(), + featureId: z.string(), + }); + +export const WorkerCompletedEntrySchema = BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.WorkerCompleted), + workerSessionId: z.string(), + featureId: z.string(), + successState: FeatureSuccessStateSchema, + returnToOrchestrator: z.boolean(), + commitId: OptionalNonBlankStringSchema, + repoPath: OptionalNonBlankStringSchema, + exitCode: z.number(), + validatorsPassed: z.boolean().optional(), + handoff: HandoffSchema.optional(), +}); + +export const WorkerFailedEntrySchema = BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.WorkerFailed), + workerSessionId: z.string().optional(), + spawnId: z.string(), + exitCode: z.number().optional(), + reason: z.string(), + /** + * Structured cause that lets the MissionRunner branch on specific failure + * modes (e.g. unrecoverable 402 → auto-pause). Absent means a generic + * failure: requeue + return to orchestrator. + */ + failureReason: z.nativeEnum(WorkerFailureReason).optional(), +}); + +export const WorkerPausedEntrySchema = BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.WorkerPaused), + workerSessionId: z.string(), + featureId: z.string().optional(), +}); + +export const HandoffItemsDismissedEntrySchema = + BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.HandoffItemsDismissed), + dismissals: z.array(DismissalRecordSchema).optional(), + }); + +export const MilestoneValidationTriggeredEntrySchema = + BaseProgressLogEntrySchema.extend({ + type: z.literal(ProgressLogEntryType.MilestoneValidationTriggered), + milestone: z.string(), + featureId: z.string(), + }); + +export const ProgressLogEntrySchema = z.discriminatedUnion('type', [ + MissionAcceptedEntrySchema, + MissionPausedEntrySchema, + MissionResumedEntrySchema, + MissionRunStartedEntrySchema, + WorkerStartedEntrySchema, + WorkerSelectedFeatureEntrySchema, + WorkerCompletedEntrySchema, + WorkerFailedEntrySchema, + WorkerPausedEntrySchema, + HandoffItemsDismissedEntrySchema, + MilestoneValidationTriggeredEntrySchema, +]); diff --git a/tests/protocol-dir.test.ts b/tests/protocol-dir.test.ts index d5cdc34..10b7096 100644 --- a/tests/protocol-dir.test.ts +++ b/tests/protocol-dir.test.ts @@ -1,16 +1,28 @@ import { describe, expect, it } from 'vitest'; +import { + CliRequestOrNotificationSchema, + RequestPermissionRequestParamsSchema, +} from '../src/protocol/cli.js'; +import { + ClientRequestSchema, + InitializeSessionRequestParamsSchema, +} from '../src/protocol/client.js'; import { FACTORY_PROTOCOL_VERSION, JSONRPC_VERSION, LEGACY_FACTORY_API_VERSION, } from '../src/protocol/constants.js'; +import { ManagedCustomModelSchema } from '../src/protocol/custom-models.js'; import { + DroidClientMethod, DroidLoopStatus, + DroidServerMethod, JsonRpcErrorCode, JsonRpcMessageType, MessageContentBlockType, MessageRole, + ModelProvider, ReasoningEffort, SessionPlatform, } from '../src/protocol/enums.js'; @@ -194,3 +206,64 @@ describe('protocol TIER-2', () => { expect(settings.workerReasoningEffort).toBe(ReasoningEffort.High); }); }); + +describe('protocol TIER-3', () => { + it('parses InitializeSessionRequestParams with required fields', () => { + const params = InitializeSessionRequestParamsSchema.parse({ + machineId: 'machine-1', + cwd: '/tmp/project', + }); + expect(params.machineId).toBe('machine-1'); + }); + + it('parses an InitializeSession client request via the discriminated union', () => { + const request = ClientRequestSchema.parse({ + ...envelope, + type: 'request', + id: 'req-1', + method: DroidServerMethod.INITIALIZE_SESSION, + params: { machineId: 'machine-1', cwd: '/tmp/project' }, + }); + expect(request.method).toBe(DroidServerMethod.INITIALIZE_SESSION); + }); + + it('parses a ManagedCustomModel with apiKey + baseUrl', () => { + const model = ManagedCustomModelSchema.parse({ + model: 'gpt-x', + provider: ModelProvider.ANTHROPIC, + baseUrl: 'https://example.com', + apiKey: 'secret', + }); + expect(model.model).toBe('gpt-x'); + }); + + it('rejects a ManagedCustomModel missing apiKey when not bedrock', () => { + expect(() => + ManagedCustomModelSchema.parse({ + model: 'gpt-x', + provider: ModelProvider.ANTHROPIC, + baseUrl: 'https://example.com', + }) + ).toThrow(); + }); + + it('parses a RequestPermission CLI request via the envelope union', () => { + const params = { + toolUses: [], + options: [], + }; + expect(RequestPermissionRequestParamsSchema.parse(params).toolUses).toEqual( + [] + ); + const message = { + ...envelope, + type: 'request', + id: 'req-2', + method: DroidClientMethod.REQUEST_PERMISSION, + params, + }; + expect(CliRequestOrNotificationSchema.parse(message).method).toBe( + DroidClientMethod.REQUEST_PERMISSION + ); + }); +});